package mcp import ( "context" "errors" "testing" sdkjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) // TestClient_Connect_NegotiatesOlderProtocolVersion 协议版本不硬编码: // 服务器只支持旧版本时,SDK 回退协商成功,协商结果保留在 ServerInfo 里。 func TestClient_Connect_NegotiatesOlderProtocolVersion(t *testing.T) { t.Parallel() srv := mcpsdk.NewServer(&mcpsdk.Implementation{Name: "old-mcp", Version: "0.9.0"}, &mcpsdk.ServerOptions{ SupportedProtocolVersions: []string{"2024-11-05"}, }) addEchoAndAdd(srv) c := connectFakeClient(t, srv) if got := c.ServerInfo().ProtocolVersion; got != "2024-11-05" { t.Fatalf("协商后的协议版本 = %q, 期望 2024-11-05", got) } tools, err := c.ListTools(context.Background()) if err != nil { t.Fatalf("ListTools: %v", err) } if len(tools) != 2 { t.Fatalf("旧协议下发现 %d 把工具, 期望 2", len(tools)) } } // TestWrapConnectErr_MapsUnsupportedVersion SDK 报的 -32022 必须映射到本包哨兵错误, // 其它错误原样透传(不误判)。 func TestWrapConnectErr_MapsUnsupportedVersion(t *testing.T) { t.Parallel() mapped := wrapConnectErr(&sdkjsonrpc.Error{Code: mcpsdk.CodeUnsupportedProtocolVersion, Message: "unsupported protocol version"}) if !errors.Is(mapped, ErrUnsupportedProtocolVersion) { t.Fatalf("wrapConnectErr = %v, 期望 ErrUnsupportedProtocolVersion", mapped) } if other := wrapConnectErr(errors.New("boom")); errors.Is(other, ErrUnsupportedProtocolVersion) { t.Fatalf("无关错误被误判:%v", other) } if other := wrapConnectErr(&sdkjsonrpc.Error{Code: -32601, Message: "method not found"}); errors.Is(other, ErrUnsupportedProtocolVersion) { t.Fatalf("其它 JSON-RPC 错误码被误判:%v", other) } } // TestClient_Connect_IdempotentAndClosed Connect 幂等;Close 后不可再连接。 func TestClient_Connect_IdempotentAndClosed(t *testing.T) { t.Parallel() c := connectFakeClient(t, newFakeServer()) if err := c.Connect(context.Background()); err != nil { t.Fatalf("重复 Connect 应幂等返回 nil, 得到 %v", err) } if err := c.Close(); err != nil { t.Fatalf("Close: %v", err) } if err := c.Close(); err != nil { t.Fatalf("重复 Close 应幂等, 得到 %v", err) } if err := c.Connect(context.Background()); !errors.Is(err, ErrClosed) { t.Fatalf("Close 后 Connect error = %v, 期望 ErrClosed", err) } } // TestClient_Connect_NoTransport 没有任何传输信息时报 ErrNoTransport。 func TestClient_Connect_NoTransport(t *testing.T) { t.Parallel() c := New(MCPConfig{Name: "none"}) if err := c.Connect(context.Background()); !errors.Is(err, ErrNoTransport) { t.Fatalf("Connect error = %v, 期望 ErrNoTransport", err) } } // TestClient_UseBeforeConnect 未连接时各入口都返回 ErrNotConnected,不 panic。 func TestClient_UseBeforeConnect(t *testing.T) { t.Parallel() c := New(MCPConfig{Name: "none", Command: "unused"}) if _, err := c.ListTools(context.Background()); !errors.Is(err, ErrNotConnected) { t.Fatalf("ListTools error = %v, 期望 ErrNotConnected", err) } if _, err := c.CallTool(context.Background(), "echo", map[string]any{}); !errors.Is(err, ErrNotConnected) { t.Fatalf("CallTool error = %v, 期望 ErrNotConnected", err) } if c.Session() != nil { t.Fatal("未连接时 Session() 应返回 nil") } if c.Name() != "none" { t.Fatalf("Name() = %q", c.Name()) } } // TestClient_CallTool_RequiresJSONObject 参数不是 JSON 对象时直接拒绝,不发请求。 func TestClient_CallTool_RequiresJSONObject(t *testing.T) { t.Parallel() c := connectFakeClient(t, newFakeServer()) if _, err := c.CallTool(context.Background(), "echo", "not-an-object"); err == nil { t.Fatal("字符串参数没有被拒绝") } }