package mcp import ( "context" "errors" "testing" ) func TestClient_New(t *testing.T) { t.Parallel() c := New(MCPConfig{Name: "calc", Command: "mcp-calc"}) if c.cfg.Name != "calc" { t.Fatalf("cfg.Name = %q, want calc", c.cfg.Name) } if c.state != stateNew { t.Fatalf("state = %v, want stateNew", c.state) } } func TestClient_Connect_FakeTransport(t *testing.T) { t.Parallel() ft := &fakeTransport{} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if ft.starts != 1 { t.Fatalf("starts = %d, want 1", ft.starts) } if !ft.hasMethod("initialize") { t.Fatalf("initialize was not called; methods = %v", ft.calledMethods()) } if c.state != stateConnected { t.Fatalf("state = %v, want stateConnected", c.state) } if c.info.Name != "fake-server" { t.Fatalf("info.Name = %q, want fake-server", c.info.Name) } } func TestClient_Connect_Idempotent(t *testing.T) { t.Parallel() ft := &fakeTransport{} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("first Connect: %v", err) } if err := c.Connect(context.Background()); err != nil { t.Fatalf("second Connect: %v", err) } if ft.starts != 1 { t.Fatalf("starts = %d, want 1 (second connect must not restart)", ft.starts) } } func TestClient_Connect_NoTransport(t *testing.T) { t.Parallel() c := New(MCPConfig{Name: "empty"}) err := c.Connect(context.Background()) if !errors.Is(err, ErrNoTransport) { t.Fatalf("Connect error = %v, want ErrNoTransport", err) } } func TestClient_Connect_Closed(t *testing.T) { t.Parallel() ft := &fakeTransport{} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Close(); err != nil { t.Fatalf("Close: %v", err) } err := c.Connect(context.Background()) if !errors.Is(err, ErrClosed) { t.Fatalf("Connect error = %v, want ErrClosed", err) } } func TestClient_Connect_StartError(t *testing.T) { t.Parallel() ft := &fakeTransport{startErr: errors.New("boom")} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err == nil { t.Fatal("Connect succeeded, want error") } if ft.closes != 1 { t.Fatalf("closes = %d, want 1 (failed Start must Close the transport)", ft.closes) } } func TestClient_Connect_InitError(t *testing.T) { t.Parallel() ft := &fakeTransport{initErr: errors.New("init boom")} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err == nil { t.Fatal("Connect succeeded, want error") } if ft.closes != 1 { t.Fatalf("closes = %d, want 1 (failed initialize must Close the transport)", ft.closes) } } func TestClient_ListTools(t *testing.T) { t.Parallel() ft := &fakeTransport{ tools: []ToolInfo{{Name: "add", Description: "Adds", InputSchema: map[string]any{"type": "object"}}}, } c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } tools, err := c.ListTools(context.Background()) if err != nil { t.Fatalf("ListTools: %v", err) } if len(tools) != 1 || tools[0].Name != "add" || tools[0].Description != "Adds" { t.Fatalf("tools = %+v, want [add/Adds]", tools) } } func TestClient_ListTools_NotConnected(t *testing.T) { t.Parallel() c := New(MCPConfig{Name: "s", Command: "x"}) _, err := c.ListTools(context.Background()) if !errors.Is(err, ErrNotConnected) { t.Fatalf("ListTools error = %v, want ErrNotConnected", err) } } func TestClient_CallTool(t *testing.T) { t.Parallel() ft := &fakeTransport{} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } res, err := c.CallTool(context.Background(), "add", map[string]any{"x": 1}) if err != nil { t.Fatalf("CallTool: %v", err) } if res.IsError { t.Fatal("IsError = true, want false") } if len(res.Content) != 1 || res.Content[0].Text != "hello" { t.Fatalf("Content = %+v, want [hello]", res.Content) } } func TestClient_CallTool_BadArgs(t *testing.T) { t.Parallel() ft := &fakeTransport{} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } _, err := c.CallTool(context.Background(), "add", "not-a-map") if err == nil { t.Fatal("CallTool with non-object args succeeded, want error") } } func TestClient_CallTool_NotConnected(t *testing.T) { t.Parallel() c := New(MCPConfig{Name: "s", Command: "x"}) _, err := c.CallTool(context.Background(), "add", map[string]any{}) if !errors.Is(err, ErrNotConnected) { t.Fatalf("CallTool error = %v, want ErrNotConnected", err) } } func TestClient_DiscoverTools_MapsToSDK(t *testing.T) { t.Parallel() ft := &fakeTransport{ tools: []ToolInfo{{Name: "add", Description: "Adds", InputSchema: map[string]any{"type": "object"}}}, } c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } tools, err := c.DiscoverTools(context.Background()) if err != nil { t.Fatalf("DiscoverTools: %v", err) } if len(tools) != 1 { t.Fatalf("len = %d, want 1", len(tools)) } if tools[0].Name() != "add" || tools[0].Description() != "Adds" { t.Fatalf("tool = %q/%q, want add/Adds", tools[0].Name(), tools[0].Description()) } params, err := tools[0].Parameters() if err != nil { t.Fatalf("Parameters: %v", err) } if params["type"] != "object" { t.Fatalf("params[type] = %v, want object", params["type"]) } out, err := tools[0].Execute(context.Background(), map[string]any{"x": 1}) if err != nil { t.Fatalf("Execute: %v", err) } if out != "hello" { t.Fatalf("Execute result = %v, want hello", out) } } func TestClient_DiscoverTools_NilSchema(t *testing.T) { t.Parallel() ft := &fakeTransport{tools: []ToolInfo{{Name: "nop", InputSchema: nil}}} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } tools, err := c.DiscoverTools(context.Background()) if err != nil { t.Fatalf("DiscoverTools: %v", err) } params, err := tools[0].Parameters() if err != nil { t.Fatalf("Parameters: %v", err) } if params["type"] != "object" { t.Fatalf("params[type] = %v, want object", params["type"]) } } func TestClient_Close_ClosesTransportAndForbidsUse(t *testing.T) { t.Parallel() ft := &fakeTransport{} c := New(MCPConfig{Name: "s", Command: "x"}, WithTransport(ft)) if err := c.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if err := c.Close(); err != nil { t.Fatalf("Close: %v", err) } if ft.closes != 1 { t.Fatalf("closes = %d, want 1", ft.closes) } if err := c.Close(); err != nil { t.Fatalf("second Close: %v", err) } if ft.closes != 1 { t.Fatalf("closes = %d, want 1 after idempotent Close", ft.closes) } if _, err := c.ListTools(context.Background()); !errors.Is(err, ErrNotConnected) { t.Fatalf("ListTools after Close = %v, want ErrNotConnected", err) } }