feat(capability/mcp): MCP 客户端(stdlib JSON-RPC over stdio/SSE)+ Registry 自动注册/Unregister(registry 增 Remove)
This commit is contained in:
1 parent
08223c06a5
commit
488cf9c79c
14 files changed
+2212
No files matched your search
@@ -0,0 +1,247 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user