文章目录
- 1. 写在最前面
- 2. 动手实现一个 MCP Client
- 2.1 How 天气查询 Client
- 2.1.1 向 Cursor 提问的艺术
- 2.1.2 最终成功展示
- 2.1.3 client 的代码
- 3. MCP 协议核心之一总结
- 3.1 SSE vs WebSocket
- 4. 碎碎念
- 5. 参考资料
1. 写在最前面
学习了 MCP Server 的实现后,刚好趁着今天又是提测的间隙,抽点时间学习一下 MCP Client 的实现。
注:千万不能让自己成为,你试一下……,你再试一下的开发……。
2. 动手实现一个 MCP Client
之前的文章中有介绍过如何实现一个 MCP Server。那这个 MCP Client 的就简单的实现为调用之前的天气查询的 MCP Server 吧。
2.1 How 天气查询 Client
考虑到 cursor 没有办法查询非当前工作区的内容,笔者只能采用最原始的方式将 MCP Server 的源码拷贝过来,直接在 MCP Client 的项目中进行使用。
2.1.1 向 Cursor 提问的艺术
经过这阶段的 Cursor 使用下来,笔者最深刻的一个感受就是提问的内容一定要具体,具体成伪代码为最佳。
错误示例:
问:你能帮我实现一个 mcp 的 client 吗?
答:其他省略,请看总结
注:明显这个解法是没有办法调用之前的天气查询的 MCP Server 的。
正确示例:
问:localhost:8080 地址的 mcp server ,调用 server 代码如下 package main ……
答:
注:这个回答就很不错了,基本符合笔者最初的构想,但是美中不足的是,还是再修复一次错误。
错误修复过程:
不得不感叹于 Cursor 强大的能力,三个问题,就实现了对天气查询 MCP Server 的调用。
2.1.2 最终成功展示
以下是 MCP 的 Client 在调用 Server 的效果展示:
2.1.3 client 的代码
mcp client 实现的代码结构:
-> mcp_client tree
.
├── README.md
├── go.mod
├── go.sum
├── main.go
├── mcp_client
└── pkg
└── client
└── client.go
2 directories, 6 files
client.go 的实现:
package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
)
// MCPResponse 定义 MCP 响应格式
type MCPResponse struct {
Type string `json:"type"`
Content interface{} `json:"content"`
}
// MCPToolResponse 定义工具响应格式
type MCPToolResponse struct {
Name string `json:"name"`
Parameters interface{} `json:"parameters,omitempty"`
Response interface{} `json:"response,omitempty"`
Error string `json:"error,omitempty"`
}
// Client 代表 MCP 客户端
type Client struct {
serverAddr string
httpClient *http.Client
connected bool
mu sync.RWMutex
}
// NewClient 创建一个新的 MCP 客户端实例
func NewClient(serverAddr string) *Client {
return &Client{
serverAddr: serverAddr,
httpClient: &http.Client{},
}
}
// Connect 连接到 MCP 服务器
func (c *Client) Connect(ctx context.Context) error {
c.mu.Lock()
defer c.mu.Unlock()
// 检查健康状态
resp, err := c.httpClient.Get(fmt.Sprintf("http://%s/health", c.serverAddr))
if err != nil {
return fmt.Errorf("服务器连接失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("服务器状态异常: %s", resp.Status)
}
c.connected = true
return nil
}
// GetWeather 获取指定城市的天气信息
func (c *Client) GetWeather(ctx context.Context, city string) (*MCPToolResponse, error) {
payload := map[string]interface{}{
"tool": "get_weather",
"parameters": map[string]string{
"city": city,
},
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("序列化请求失败: %v", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("http://%s/sse/invoke", c.serverAddr), bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
var response MCPToolResponse
if err := json.Unmarshal(body, &response); err != nil {
return nil, fmt.Errorf("解析响应失败: %v", err)
}
return &response, nil
}
// Status 获取服务器状态
func (c *Client) Status(ctx context.Context) (string, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if !c.connected {
return "未连接", nil
}
resp, err := c.httpClient.Get(fmt.Sprintf("http://%s/health", c.serverAddr))
if err != nil {
return "", fmt.Errorf("检查服务器状态失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return fmt.Sprintf("已连接到 %s,服务器状态正常", c.serverAddr), nil
}
return fmt.Sprintf("已连接到 %s,但服务器状态异常: %s", c.serverAddr, resp.Status), nil
}
// Close 关闭客户端连接
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.connected = false
return nil
}
main.go 的实现:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"example/mcp_client/pkg/client"
"github.com/spf13/cobra"
)
var (
serverAddr string
mcpClient *client.Client
)
var rootCmd = &cobra.Command{
Use: "mcp-client",
Short: "MCP Client - A command line tool for interacting with MCP server",
Long: `MCP Client is a command line tool that allows you to interact with the MCP (Master Control Program) server.
It provides various commands for managing and monitoring your MCP resources.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
mcpClient = client.NewClient(serverAddr)
ctx := context.Background()
if err := mcpClient.Connect(ctx); err != nil {
fmt.Printf("连接服务器失败: %v\n", err)
os.Exit(1)
}
},
}
var statusCmd = &cobra.Command{
Use: "status",
Short: "Check the status of MCP server",
RunE: func(cmd *cobra.Command, args []string) error {
status, err := mcpClient.Status(context.Background())
if err != nil {
return err
}
fmt.Println(status)
return nil
},
}
var weatherCmd = &cobra.Command{
Use: "weather [city]",
Short: "Get weather information for a city",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
city := "北京" // 默认城市
if len(args) > 0 {
city = args[0]
}
response, err := mcpClient.GetWeather(context.Background(), city)
if err != nil {
return err
}
if response.Error != "" {
return fmt.Errorf("获取天气信息失败: %s", response.Error)
}
// 格式化输出天气信息
fmt.Printf("%s的天气信息:\n", city)
fmt.Printf("%+v\n", response.Response)
return nil
},
}
func init() {
rootCmd.PersistentFlags().StringVarP(&serverAddr, "server", "s", "localhost:8080", "MCP server address")
rootCmd.AddCommand(statusCmd)
rootCmd.AddCommand(weatherCmd)
}
func main() {
// 设置信号处理
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// 在后台监听信号
go func() {
<-sigChan
fmt.Println("\n正在关闭客户端...")
if err := mcpClient.Close(); err != nil {
fmt.Printf("关闭客户端时发生错误: %v\n", err)
}
os.Exit(0)
}()
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
3. MCP 协议核心之一总结
笔者通过动手实现一对极简的 MCP Server 和 MCP Client 发现,SSE(Server-Sent Events)协议应该是 MCP 协议实现的基石之一。
于是,笔者翻阅了 MCP 的官方文档说明,得出 — MCP includes two standard transport implementations:
-
Standard Input/Output (stdio)
-
Server-Sent Events (SSE)
3.1 SSE vs WebSocket
笔者看到 SSE 协议是基于 http 实现的,不仅好奇 SSE 跟 WebSocket 协议的区别:
Curosr 总结的 SSE 和 WebSocket 的对比:
-
通信方式:
-
SSE:单向通信,服务器向客户端推送数据
-
WebSocket:双向通信,客户端和服务器都可以主动发送数据
-
-
协议:
-
SSE:基于 HTTP,使用标准的 HTTP 连接
-
WebSocket:独立的协议,使用 ws:// 或 wss:// 协议前缀
-
-
连接特性:
-
SSE:自动重连,浏览器实现了断线重连机制
-
WebSocket:需要手动实现重连逻辑
-
-
数据格式:
-
SSE:仅支持文本数据(UTF-8)
-
WebSocket:支持文本和二进制数据
-
-
使用场景:
-
SSE:适用于服务器到客户端的实时通知、事件流
-
WebSocket:适用于需要低延迟、高频率双向通信的场景
-
-
兼容性:
-
SSE:较好的浏览器兼容性,但IE不支持
-
WebSocket:现代浏览器都支持
-
4. 碎碎念
做人不能太贪心,就像一口没办法吃成一个胖子,知识也不是一天就能学完的,那就慢慢积累着吧!
-
不妨大胆点,反正没人能活着离开这个世界。
-
突然觉得自己真的很好,有点小漂亮,善解人意,懂得换位思考,分享欲比较旺盛,三观正,待人真诚,自愈能力也强。即使自己情绪不好陷入内耗也还是会倾听他人的烦恼开导别人。能在无数次的崩溃中慢慢自愈,能在滥情的世界里始终保持清醒,但别否定和怀疑自己啦,你真的超棒的。
5. 参考资料
- Transports - Model Context Protocol