client.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. // Package mcp 提供 MCP(Model Context Protocol)客户端,用于通过 MCP 协议调用远程工具(如 Serper 搜索)。
  2. package mcp
  3. import (
  4. "context"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "sync"
  9. "github.com/modelcontextprotocol/go-sdk/mcp"
  10. )
  11. // ErrNotConfigured 表示未配置 MCP 服务 URL 或未成功连接。
  12. var ErrNotConfigured = errors.New("mcp: server URL not configured or not connected")
  13. // Client 封装对单个 MCP 服务端的连接,支持 CallTool。
  14. type Client struct {
  15. serverURL string
  16. impl *mcp.Implementation
  17. transport *mcp.StreamableClientTransport
  18. client *mcp.Client
  19. session *mcp.ClientSession
  20. mu sync.Mutex
  21. }
  22. // NewClient 创建一个 MCP 客户端。serverURL 为 MCP 服务 HTTP/SSE 地址(如 http://localhost:3000/sse)。
  23. // 若 serverURL 为空,后续 Connect/CallTool 将返回 ErrNotConfigured。
  24. func NewClient(serverURL string) *Client {
  25. url := strings.TrimSpace(serverURL)
  26. return &Client{
  27. serverURL: url,
  28. impl: &mcp.Implementation{Name: "ai-cs-backend", Version: "v1.0.0"},
  29. }
  30. }
  31. // Connect 连接到 MCP 服务端。应在调用 CallTool 前成功调用一次。
  32. func (c *Client) Connect(ctx context.Context) error {
  33. c.mu.Lock()
  34. defer c.mu.Unlock()
  35. if c.serverURL == "" {
  36. return ErrNotConfigured
  37. }
  38. if c.session != nil {
  39. return nil // 已连接
  40. }
  41. c.transport = &mcp.StreamableClientTransport{Endpoint: c.serverURL}
  42. c.client = mcp.NewClient(c.impl, nil)
  43. session, err := c.client.Connect(ctx, c.transport, nil)
  44. if err != nil {
  45. return fmt.Errorf("mcp connect: %w", err)
  46. }
  47. c.session = session
  48. return nil
  49. }
  50. // CallTool 调用远程工具。name 为工具名(如 google_search),args 为参数(如 map[string]any{"query": "..."})。
  51. // 返回工具结果中的文本内容拼接;若未连接或工具返回错误则返回错误。
  52. func (c *Client) CallTool(ctx context.Context, name string, args map[string]any) (string, error) {
  53. c.mu.Lock()
  54. session := c.session
  55. c.mu.Unlock()
  56. if c.serverURL == "" {
  57. return "", ErrNotConfigured
  58. }
  59. if session == nil {
  60. return "", ErrNotConfigured
  61. }
  62. params := &mcp.CallToolParams{Name: name, Arguments: args}
  63. result, err := session.CallTool(ctx, params)
  64. if err != nil {
  65. return "", fmt.Errorf("mcp call tool %s: %w", name, err)
  66. }
  67. if result.IsError {
  68. // 工具侧错误,从 Content 中取错误信息
  69. text := extractTextContent(result.Content)
  70. if text != "" {
  71. return "", fmt.Errorf("mcp tool error: %s", text)
  72. }
  73. return "", fmt.Errorf("mcp tool error (no content)")
  74. }
  75. return extractTextContent(result.Content), nil
  76. }
  77. // Close 关闭与 MCP 服务端的会话。
  78. func (c *Client) Close() error {
  79. c.mu.Lock()
  80. defer c.mu.Unlock()
  81. if c.session == nil {
  82. return nil
  83. }
  84. err := c.session.Close()
  85. c.session = nil
  86. return err
  87. }
  88. func extractTextContent(content []mcp.Content) string {
  89. var b strings.Builder
  90. for _, c := range content {
  91. if tc, ok := c.(*mcp.TextContent); ok {
  92. b.WriteString(tc.Text)
  93. }
  94. }
  95. return b.String()
  96. }