248 lines
7.1 KiB
Go
248 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|