@@ -8,8 +8,13 @@ import (
8
8
"context"
9
9
"fmt"
10
10
"log"
11
+ "slices"
12
+ "testing"
11
13
14
+ "github.com/google/go-cmp/cmp"
15
+ "github.com/google/go-cmp/cmp/cmpopts"
12
16
"golang.org/x/tools/internal/mcp"
17
+ "golang.org/x/tools/internal/mcp/jsonschema"
13
18
)
14
19
15
20
type SayHiParams struct {
@@ -51,3 +56,159 @@ func ExampleServer() {
51
56
52
57
// Output: Hi user
53
58
}
59
+
60
+ func TestListTool (t * testing.T ) {
61
+ toolA := mcp .NewTool ("apple" , "apple tool" , SayHi )
62
+ toolB := mcp .NewTool ("banana" , "banana tool" , SayHi )
63
+ toolC := mcp .NewTool ("cherry" , "cherry tool" , SayHi )
64
+ testCases := []struct {
65
+ tools []* mcp.ServerTool
66
+ want []* mcp.Tool
67
+ pageSize int
68
+ }{
69
+ {
70
+ // Simple test.
71
+ []* mcp.ServerTool {toolA , toolB , toolC },
72
+ []* mcp.Tool {toolA .Tool , toolB .Tool , toolC .Tool },
73
+ mcp .DefaultPageSize ,
74
+ },
75
+ {
76
+ // Tools should be ordered by tool name.
77
+ []* mcp.ServerTool {toolC , toolA , toolB },
78
+ []* mcp.Tool {toolA .Tool , toolB .Tool , toolC .Tool },
79
+ mcp .DefaultPageSize ,
80
+ },
81
+ {
82
+ // Page size of 1 should yield the first tool only.
83
+ []* mcp.ServerTool {toolC , toolA , toolB },
84
+ []* mcp.Tool {toolA .Tool },
85
+ 1 ,
86
+ },
87
+ {
88
+ // Page size of 2 should yield the first 2 tools only.
89
+ []* mcp.ServerTool {toolC , toolA , toolB },
90
+ []* mcp.Tool {toolA .Tool , toolB .Tool },
91
+ 2 ,
92
+ },
93
+ {
94
+ // Page size of 3 should yield all tools.
95
+ []* mcp.ServerTool {toolC , toolA , toolB },
96
+ []* mcp.Tool {toolA .Tool , toolB .Tool , toolC .Tool },
97
+ 3 ,
98
+ },
99
+ {
100
+ []* mcp.ServerTool {},
101
+ nil ,
102
+ 1 ,
103
+ },
104
+ }
105
+ ctx := context .Background ()
106
+ for _ , tc := range testCases {
107
+ server := mcp .NewServer ("server" , "v0.0.1" , & mcp.ServerOptions {PageSize : tc .pageSize })
108
+ server .AddTools (tc .tools ... )
109
+ clientTransport , serverTransport := mcp .NewInMemoryTransports ()
110
+ serverSession , err := server .Connect (ctx , serverTransport )
111
+ if err != nil {
112
+ log .Fatal (err )
113
+ }
114
+ client := mcp .NewClient ("client" , "v0.0.1" , nil )
115
+ clientSession , err := client .Connect (ctx , clientTransport )
116
+ if err != nil {
117
+ log .Fatal (err )
118
+ }
119
+ res , err := clientSession .ListTools (ctx , nil )
120
+ serverSession .Close ()
121
+ clientSession .Close ()
122
+ if err != nil {
123
+ log .Fatal (err )
124
+ }
125
+ if len (res .Tools ) != len (tc .want ) {
126
+ t .Fatalf ("expected %d tools, got %d" , len (tc .want ), len (res .Tools ))
127
+ }
128
+ if diff := cmp .Diff (res .Tools , tc .want , cmpopts .IgnoreUnexported (jsonschema.Schema {})); diff != "" {
129
+ t .Fatalf ("expected tools %+v, got %+v" , tc .want , res .Tools )
130
+ }
131
+ if tc .pageSize < len (tc .tools ) && res .NextCursor == "" {
132
+ t .Fatalf ("expected next cursor, got none" )
133
+ }
134
+ }
135
+ }
136
+
137
+ func TestListToolPaginateInvalidCursor (t * testing.T ) {
138
+ toolA := mcp .NewTool ("apple" , "apple tool" , SayHi )
139
+ ctx := context .Background ()
140
+ server := mcp .NewServer ("server" , "v0.0.1" , nil )
141
+ server .AddTools (toolA )
142
+ clientTransport , serverTransport := mcp .NewInMemoryTransports ()
143
+ serverSession , err := server .Connect (ctx , serverTransport )
144
+ if err != nil {
145
+ log .Fatal (err )
146
+ }
147
+ client := mcp .NewClient ("client" , "v0.0.1" , nil )
148
+ clientSession , err := client .Connect (ctx , clientTransport )
149
+ if err != nil {
150
+ log .Fatal (err )
151
+ }
152
+ _ , err = clientSession .ListTools (ctx , & mcp.ListToolsParams {Cursor : "invalid" })
153
+ if err == nil {
154
+ t .Fatalf ("expected error, got none" )
155
+ }
156
+ serverSession .Close ()
157
+ clientSession .Close ()
158
+ }
159
+
160
+ func TestListToolPaginate (t * testing.T ) {
161
+ serverTools := []* mcp.ServerTool {
162
+ mcp .NewTool ("apple" , "apple tool" , SayHi ),
163
+ mcp .NewTool ("banana" , "banana tool" , SayHi ),
164
+ mcp .NewTool ("cherry" , "cherry tool" , SayHi ),
165
+ mcp .NewTool ("durian" , "durian tool" , SayHi ),
166
+ mcp .NewTool ("elderberry" , "elderberry tool" , SayHi ),
167
+ }
168
+ var wantTools []* mcp.Tool
169
+ for _ , tool := range serverTools {
170
+ wantTools = append (wantTools , tool .Tool )
171
+ }
172
+ ctx := context .Background ()
173
+ // Try all possible page sizes, ensuring we get the correct list of tools.
174
+ for pageSize := 1 ; pageSize < len (serverTools )+ 1 ; pageSize ++ {
175
+ server := mcp .NewServer ("server" , "v0.0.1" , & mcp.ServerOptions {PageSize : pageSize })
176
+ server .AddTools (serverTools ... )
177
+ clientTransport , serverTransport := mcp .NewInMemoryTransports ()
178
+ serverSession , err := server .Connect (ctx , serverTransport )
179
+ if err != nil {
180
+ log .Fatal (err )
181
+ }
182
+ client := mcp .NewClient ("client" , "v0.0.1" , nil )
183
+ clientSession , err := client .Connect (ctx , clientTransport )
184
+ if err != nil {
185
+ log .Fatal (err )
186
+ }
187
+ var gotTools []* mcp.Tool
188
+ var nextCursor string
189
+ wantChunks := slices .Collect (slices .Chunk (wantTools , pageSize ))
190
+ index := 0
191
+ // Iterate through all pages, comparing sub-slices to the paginated list.
192
+ for {
193
+ res , err := clientSession .ListTools (ctx , & mcp.ListToolsParams {Cursor : nextCursor })
194
+ if err != nil {
195
+ log .Fatal (err )
196
+ }
197
+ gotTools = append (gotTools , res .Tools ... )
198
+ nextCursor = res .NextCursor
199
+ if diff := cmp .Diff (res .Tools , wantChunks [index ], cmpopts .IgnoreUnexported (jsonschema.Schema {})); diff != "" {
200
+ t .Errorf ("expected %v, got %v, (-want +got):\n %s" , wantChunks [index ], res .Tools , diff )
201
+ }
202
+ if res .NextCursor == "" {
203
+ break
204
+ }
205
+ index ++
206
+ }
207
+ serverSession .Close ()
208
+ clientSession .Close ()
209
+
210
+ if len (gotTools ) != len (wantTools ) {
211
+ t .Fatalf ("expected %d tools, got %d" , len (wantTools ), len (gotTools ))
212
+ }
213
+ }
214
+ }
0 commit comments