让 Cursor 教我写 MCP Client

news2025/7/19 2:48:46

文章目录

    • 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2376437.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

反射, 注解, 动态代理

文章目录 单元测试什么是单元测试咱们之前是如何进行单元测试的&#xff1f; 有啥问题 &#xff1f;现在使用方法进行测试优点Junit单元测试的使用步骤删除不需要的jar包总结 反射认识反射、获取类什么是反射反射具体学什么&#xff1f;反射第一步&#xff1a;或者Class对象 获…

vue vite 无法热更新问题

一、在vue页面引入组件CustomEmployeesDialog&#xff0c;修改组件CustomEmployeesDialog无法热更新 引入方式&#xff1a; import CustomEmployeesDialog from ../dialog/customEmployeesDialog.vue 目录结构&#xff1a; 最后发现是引入import时&#xff0c;路径大小写与目…

深度学习中的查全率与查准率:如何实现有效权衡

&#x1f4cc; 友情提示&#xff1a; 本文内容由银河易创AI&#xff08;https://ai.eaigx.com&#xff09;创作平台的gpt-4-turbo模型辅助生成&#xff0c;旨在提供技术参考与灵感启发。文中观点或代码示例需结合实际情况验证&#xff0c;建议读者通过官方文档或实践进一步确认…

Windows玩游戏的时候,一按字符键就显示桌面

最近打赛伯朋克 2077 的时候&#xff0c;不小心按错键了&#xff0c;导致一按字符键就显示桌面。如下&#xff1a; 一开始我以为是输入法的问题&#xff08;相信打游戏的人都知道输入法和奔跑键冲突的时候有多烦&#xff09;&#xff0c;但是后来解决半天发现并不是。在网上搜…

Gemini 2.5 Flash和Pro预览版价格以及上下文缓存的理解

Gemini 2.5 Flash和Pro预览版价格 Gemini 2.5 Flash 预览版就是 Google 的最新 AI 大模型&#xff0c;能处理巨量内容。可以免费体验&#xff0c;但有次数和功能上的限制&#xff1b;付费层级才开放全部高级功能。价格也比传统 API 略有不同&#xff0c;尤其在“思考预算”“上…

vue2 头像上传+裁剪组件封装

背景&#xff1a;最近在进行公司业务开发时&#xff0c;遇到了头像上传限制尺寸的需求&#xff0c;即限制为一寸证件照&#xff08;宽295像素&#xff0c;高413像素&#xff09;。 用到的第三方库&#xff1a; "vue-cropper": "^0.5.5" 完整组件代码&…

AI-02a5a5.神经网络-与学习相关的技巧-权重初始值

权重的初始值 在神经网络的学习中&#xff0c;权重的初始值特别重要。实际上&#xff0c;设定什么样的权重初始值&#xff0c;经常关系到神经网络的学习能否成功。 不要将权重初始值设为 0 权值衰减&#xff08;weight decay&#xff09;&#xff1a;抑制过拟合、提高泛化能…

【springcloud学习(dalston.sr1)】Eureka单个服务端的搭建(含源代码)(三)

该系列项目整体介绍及源代码请参照前面写的一篇文章【springcloud学习(dalston.sr1)】项目整体介绍&#xff08;含源代码&#xff09;&#xff08;一&#xff09; 这篇文章主要介绍单个eureka服务端的集群环境是如何搭建的。 通过前面的文章【springcloud学习(dalston.sr1)】…

Node.js数据抓取技术实战示例

Node.js常用的库有哪些呢&#xff1f;比如axios或者node-fetch用来发送HTTP请求&#xff0c;cheerio用来解析HTML&#xff0c;如果是动态网页的话可能需要puppeteer这样的无头浏览器。这些工具的组合应该能满足大部分需求。 然后&#xff0c;可能遇到的难点在哪里&#xff1f;…

windows10 安装 QT

本地环境有个qt文件&#xff0c;这里是5.14.2 打开一个cmd窗口并指定到该文件根目录下 .\qt-opensource-windows-x86-5.14.2.exe --mirror https://mirrors.ustc.edu.cn/qtproject 执行上面命令 记住是文件名&#xff0c;记住不要傻 X的直接复制&#xff0c;是你的文件名 点击…

WordPress 和 GPL – 您需要了解的一切

如果您使用 WordPress&#xff0c;GPL 对您来说应该很重要&#xff0c;您也应该了解它。查看有关 WordPress 和 GPL 的最全面指南。 您可能听说过 GPL&#xff08;通常被称为 WordPress 的权利法案&#xff09;&#xff0c;但很可能并不完全了解它。这是有道理的–这是一个复杂…

C++书本摆放 2024年信息素养大赛复赛 C++小学/初中组 算法创意实践挑战赛 真题详细解析

目录 C++书本摆放 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、运行结果 五、考点分析 六、 推荐资料 1、C++资料 2、Scratch资料 3、Python资料 C++书本摆放 2024年信息素养大赛 C++复赛真题 一、题目要求 1、编程实现 中科智慧科技…

RabbitMQ 核心概念与消息模型深度解析(一)

一、RabbitMQ 是什么 在当今分布式系统盛行的时代&#xff0c;消息队列作为一种至关重要的中间件技术&#xff0c;扮演着实现系统之间异步通信、解耦和削峰填谷等关键角色 。RabbitMQ 便是消息队列领域中的佼佼者&#xff0c;是一个开源的消息代理和队列服务器&#xff0c;基于…

论文阅读笔记——双流网络

双流网络论文 视频相比图像包含更多信息&#xff1a;运动信息、时序信息、背景信息等等。 原先处理视频的方法&#xff1a; CNN LSTM&#xff1a;CNN 抽取关键特征&#xff0c;LSTM 做时序逻辑&#xff1b;抽取视频中关键 K 帧输入 CNN 得到图片特征&#xff0c;再输入 LSTM&…

LabVIEW在电子电工教学中的应用

在电子电工教学领域&#xff0c;传统教学模式面临诸多挑战&#xff0c;如实验设备数量有限、实验过程存在安全隐患、教学内容更新滞后等。LabVIEW 作为一款功能强大的图形化编程软件&#xff0c;为解决这些问题提供了创新思路&#xff0c;在电子电工教学的多个关键环节发挥着重…

Vue3 怎么在ElMessage消息提示组件中添加自定义icon图标

1、定义icon组件代码&#xff1a; <template><svg :class"svgClass" aria-hidden"true"><use :xlink:href"iconName" :fill"color"/></svg> </template><script> export default defineComponen…

生活破破烂烂,AI 缝缝补补(附提示词)

写在前面&#xff1a;​【Fire 计算器】已上线&#xff0c;快算算财富自由要多少​ 现实不总温柔&#xff0c;愿你始终自渡。 请永远拯救自己于水火之中。 毛绒风格提示词&#xff08;供参考&#xff09;&#xff1a; 1. 逼真毛绒风 Transform this image into a hyperrealist…

张 。。 通过Token实现Loss调优prompt

词编码模型和 API LLM不匹配,采用本地模型 理性中性案例(针对中性调整比较合理) 代码解释:Qwen2模型的文本编码与生成过程 这段代码展示了如何使用Qwen2模型进行文本的编码和解码操作。 模型加载与初始化 from transformers import AutoModelForCausalLM, AutoTokenizer

JVM学习专题(一)类加载器与双亲委派

目录 1、JVM加载运行全过程梳理 2、JVM Hotspot底层 3、war包、jar包如何加载 4、类加载器 我们来查看一下getLauncher&#xff1a; 1.我们先查看getExtClassLoader() 2、再来看看getAppClassLoader(extcl) 5、双亲委派机制 1.职责明确&#xff0c;路径隔离​&#xff…

PyTorch API 9 - masked, nested, 稀疏, 存储

文章目录 torch.randomtorch.masked简介动机什么是 MaskedTensor&#xff1f; 支持的运算符一元运算符二元运算符归约操作查看与选择函数 torch.nested简介构造方法数据布局与形状支持的操作查看嵌套张量的组成元素填充张量的相互转换形状操作注意力机制 与 torch.compile 的配…