1+ package main
2+
3+ import (
4+ "bufio"
5+ "bytes"
6+ "encoding/json"
7+ "fmt"
8+ "io"
9+ "strings"
10+ "testing"
11+
12+ "github.com/f/mcptools/pkg/transport"
13+ "github.com/spf13/cobra"
14+ )
15+
16+ // MockTransport implements the Transport interface for testing
17+ type MockTransport struct {
18+ Responses map [string ]map [string ]interface {}
19+ }
20+
21+ func NewMockTransport () * MockTransport {
22+ return & MockTransport {
23+ Responses : map [string ]map [string ]interface {}{},
24+ }
25+ }
26+
27+ func (t * MockTransport ) Execute (method string , params interface {}) (map [string ]interface {}, error ) {
28+ if resp , ok := t .Responses [method ]; ok {
29+ return resp , nil
30+ }
31+
32+ if method == "tools/list" {
33+ return map [string ]interface {}{
34+ "tools" : []map [string ]interface {}{
35+ {
36+ "name" : "test_tool" ,
37+ "description" : "A test tool" ,
38+ },
39+ {
40+ "name" : "another_tool" ,
41+ "description" : "Another test tool" ,
42+ },
43+ },
44+ }, nil
45+ }
46+
47+ if method == "tools/call" {
48+ paramsMap := params .(map [string ]interface {})
49+ toolName := paramsMap ["name" ].(string )
50+ return map [string ]interface {}{
51+ "result" : fmt .Sprintf ("Called tool: %s" , toolName ),
52+ }, nil
53+ }
54+
55+ if method == "resources/list" {
56+ return map [string ]interface {}{
57+ "resources" : []map [string ]interface {}{
58+ {
59+ "uri" : "test_resource" ,
60+ "description" : "A test resource" ,
61+ },
62+ },
63+ }, nil
64+ }
65+
66+ if method == "resources/read" {
67+ paramsMap := params .(map [string ]interface {})
68+ uri := paramsMap ["uri" ].(string )
69+ return map [string ]interface {}{
70+ "content" : fmt .Sprintf ("Content of resource: %s" , uri ),
71+ }, nil
72+ }
73+
74+ if method == "prompts/list" {
75+ return map [string ]interface {}{
76+ "prompts" : []map [string ]interface {}{
77+ {
78+ "name" : "test_prompt" ,
79+ "description" : "A test prompt" ,
80+ },
81+ },
82+ }, nil
83+ }
84+
85+ if method == "prompts/get" {
86+ paramsMap := params .(map [string ]interface {})
87+ promptName := paramsMap ["name" ].(string )
88+ return map [string ]interface {}{
89+ "content" : fmt .Sprintf ("Content of prompt: %s" , promptName ),
90+ }, nil
91+ }
92+
93+ return map [string ]interface {}{}, fmt .Errorf ("unknown method: %s" , method )
94+ }
95+
96+ // Helper function to create a root command with a mock transport
97+ func setupTestCommand () (* cobra.Command , * bytes.Buffer ) {
98+ // Setup output buffer
99+ outBuf := & bytes.Buffer {}
100+
101+ // Create root command
102+ rootCmd := & cobra.Command {
103+ Use : "mcp" ,
104+ Short : "MCP CLI" ,
105+ }
106+
107+ // Create shell command
108+ shellCmd := & cobra.Command {
109+ Use : "shell" ,
110+ Short : "Interactive shell" ,
111+ Run : func (cmd * cobra.Command , args []string ) {
112+ // Create mock transport
113+ mockTransport := NewMockTransport ()
114+
115+ // Create shell instance
116+ shell := & Shell {
117+ Transport : mockTransport ,
118+ Format : "table" ,
119+ Reader : strings .NewReader ("tools\n call test_tool --params '{\" foo\" :\" bar\" }'\n test_tool {\" foo\" :\" bar\" }\n resource:test_resource\n prompt:test_prompt\n /q\n " ),
120+ Writer : outBuf ,
121+ }
122+
123+ // Run shell
124+ shell .Run ()
125+ },
126+ }
127+
128+ rootCmd .AddCommand (shellCmd )
129+
130+ // Return command and output buffer
131+ return rootCmd , outBuf
132+ }
133+
134+ // Shell represents a test shell instance
135+ type Shell struct {
136+ Transport transport.Transport
137+ Format string
138+ Reader io.Reader
139+ Writer io.Writer
140+ }
141+
142+ // Run executes the shell with predefined inputs
143+ func (s * Shell ) Run () {
144+ scanner := bufio .NewScanner (s .Reader )
145+
146+ for scanner .Scan () {
147+ input := scanner .Text ()
148+
149+ if input == "/q" || input == "/quit" || input == "exit" {
150+ fmt .Fprintln (s .Writer , "Exiting MCP shell" )
151+ break
152+ }
153+
154+ parts := strings .Fields (input )
155+ if len (parts ) == 0 {
156+ continue
157+ }
158+
159+ command := parts [0 ]
160+ args := parts [1 :]
161+
162+ switch command {
163+ case "tools" :
164+ resp , _ := s .Transport .Execute ("tools/list" , nil )
165+ fmt .Fprintln (s .Writer , "Tools:" , resp )
166+
167+ case "resources" :
168+ resp , _ := s .Transport .Execute ("resources/list" , nil )
169+ fmt .Fprintln (s .Writer , "Resources:" , resp )
170+
171+ case "prompts" :
172+ resp , _ := s .Transport .Execute ("prompts/list" , nil )
173+ fmt .Fprintln (s .Writer , "Prompts:" , resp )
174+
175+ case "call" :
176+ if len (args ) < 1 {
177+ fmt .Fprintln (s .Writer , "Usage: call <entity> [--params '{...}']" )
178+ continue
179+ }
180+
181+ entityName := args [0 ]
182+ entityType := "tool"
183+
184+ parts := strings .SplitN (entityName , ":" , 2 )
185+ if len (parts ) == 2 {
186+ entityType = parts [0 ]
187+ entityName = parts [1 ]
188+ }
189+
190+ params := map [string ]interface {}{}
191+
192+ for i := 1 ; i < len (args ); i ++ {
193+ if args [i ] == "--params" || args [i ] == "-p" {
194+ if i + 1 < len (args ) {
195+ _ = json .Unmarshal ([]byte (args [i + 1 ]), & params )
196+ break
197+ }
198+ }
199+ }
200+
201+ var resp map [string ]interface {}
202+
203+ switch entityType {
204+ case "tool" :
205+ resp , _ = s .Transport .Execute ("tools/call" , map [string ]interface {}{
206+ "name" : entityName ,
207+ "arguments" : params ,
208+ })
209+ case "resource" :
210+ resp , _ = s .Transport .Execute ("resources/read" , map [string ]interface {}{
211+ "uri" : entityName ,
212+ })
213+ case "prompt" :
214+ resp , _ = s .Transport .Execute ("prompts/get" , map [string ]interface {}{
215+ "name" : entityName ,
216+ })
217+ }
218+
219+ fmt .Fprintln (s .Writer , "Call result:" , resp )
220+
221+ default :
222+ // Try to interpret as a direct tool call
223+ entityName := command
224+ entityType := "tool"
225+
226+ parts := strings .SplitN (entityName , ":" , 2 )
227+ if len (parts ) == 2 {
228+ entityType = parts [0 ]
229+ entityName = parts [1 ]
230+ }
231+
232+ params := map [string ]interface {}{}
233+
234+ if len (args ) > 0 {
235+ firstArg := args [0 ]
236+ if strings .HasPrefix (firstArg , "{" ) && strings .HasSuffix (firstArg , "}" ) {
237+ _ = json .Unmarshal ([]byte (firstArg ), & params )
238+ } else {
239+ for i := 0 ; i < len (args ); i ++ {
240+ if args [i ] == "--params" || args [i ] == "-p" {
241+ if i + 1 < len (args ) {
242+ _ = json .Unmarshal ([]byte (args [i + 1 ]), & params )
243+ break
244+ }
245+ }
246+ }
247+ }
248+ }
249+
250+ var resp map [string ]interface {}
251+
252+ switch entityType {
253+ case "tool" :
254+ resp , _ = s .Transport .Execute ("tools/call" , map [string ]interface {}{
255+ "name" : entityName ,
256+ "arguments" : params ,
257+ })
258+ fmt .Fprintln (s .Writer , "Direct tool call result:" , resp )
259+ case "resource" :
260+ resp , _ = s .Transport .Execute ("resources/read" , map [string ]interface {}{
261+ "uri" : entityName ,
262+ })
263+ fmt .Fprintln (s .Writer , "Direct resource read result:" , resp )
264+ case "prompt" :
265+ resp , _ = s .Transport .Execute ("prompts/get" , map [string ]interface {}{
266+ "name" : entityName ,
267+ })
268+ fmt .Fprintln (s .Writer , "Direct prompt get result:" , resp )
269+ default :
270+ fmt .Fprintln (s .Writer , "Unknown command:" , command )
271+ }
272+ }
273+ }
274+ }
275+
276+ // TestDirectToolCalling tests the direct tool calling functionality
277+ func TestDirectToolCalling (t * testing.T ) {
278+ testCases := []struct {
279+ input string
280+ expectedOutput string
281+ }{
282+ {
283+ input : "test_tool {\" param\" : \" value\" }" ,
284+ expectedOutput : "Called tool: test_tool" ,
285+ },
286+ {
287+ input : "resource:test_resource" ,
288+ expectedOutput : "Content of resource: test_resource" ,
289+ },
290+ {
291+ input : "prompt:test_prompt" ,
292+ expectedOutput : "Content of prompt: test_prompt" ,
293+ },
294+ }
295+
296+ // Create mock transport
297+ mockTransport := NewMockTransport ()
298+
299+ for _ , tc := range testCases {
300+ t .Run (tc .input , func (t * testing.T ) {
301+ // Setup output capture
302+ outBuf := & bytes.Buffer {}
303+
304+ // Create shell with input
305+ shell := & Shell {
306+ Transport : mockTransport ,
307+ Format : "table" ,
308+ Reader : strings .NewReader (tc .input + "\n /q\n " ),
309+ Writer : outBuf ,
310+ }
311+
312+ // Run shell
313+ shell .Run ()
314+
315+ // Check output
316+ if ! strings .Contains (outBuf .String (), tc .expectedOutput ) {
317+ t .Errorf ("Expected output to contain %q, got: %s" , tc .expectedOutput , outBuf .String ())
318+ }
319+ })
320+ }
321+ }
322+
323+ // TestExecuteShell tests the shell command execution with various commands
324+ func TestExecuteShell (t * testing.T ) {
325+ // Create mock transport
326+ mockTransport := NewMockTransport ()
327+
328+ // Test inputs
329+ inputs := []string {
330+ "tools" ,
331+ "resources" ,
332+ "prompts" ,
333+ "call test_tool --params '{\" foo\" :\" bar\" }'" ,
334+ "test_tool {\" foo\" :\" bar\" }" ,
335+ "resource:test_resource" ,
336+ "prompt:test_prompt" ,
337+ "/q" ,
338+ }
339+
340+ // Expected outputs for each input
341+ expectedOutputs := []string {
342+ "A test tool" , // tools command
343+ "A test resource" , // resources command
344+ "A test prompt" , // prompts command
345+ "Called tool: test_tool" , // call command
346+ "Called tool: test_tool" , // direct tool call
347+ "Content of resource: test_resource" , // direct resource read
348+ "Content of prompt: test_prompt" , // direct prompt get
349+ "Exiting MCP shell" , // quit command
350+ }
351+
352+ // Setup output capture
353+ outBuf := & bytes.Buffer {}
354+
355+ // Create shell with all inputs
356+ shell := & Shell {
357+ Transport : mockTransport ,
358+ Format : "table" ,
359+ Reader : strings .NewReader (strings .Join (inputs , "\n " ) + "\n " ),
360+ Writer : outBuf ,
361+ }
362+
363+ // Run shell
364+ shell .Run ()
365+
366+ // Check all expected outputs
367+ output := outBuf .String ()
368+ for _ , expected := range expectedOutputs {
369+ if ! strings .Contains (output , expected ) {
370+ t .Errorf ("Expected output to contain %q, but it doesn't.\n Full output: %s" , expected , output )
371+ }
372+ }
373+ }
0 commit comments