当前位置: 首页 > news >正文

让 Cursor 教我写 MCP Server

文章目录

    • 1. 写在最前面
    • 2. 动手实现一个 MCP Server
      • 2.1 Why 天气查询
      • 2.2 What 天气查询
      • 2.3 How 天气查询
        • 2.3.1 配置 MCP Server
        • 2.3.2 实现 MCP Server
    • 3. 碎碎念
    • 4. 参考资料

1. 写在最前面

「纸上得来终觉浅,绝知此事要躬行」在研究如何写 mcp server 的时候,脑子里突然蹦出来这么一句诗。

五一假期前趁着有空,浅浅的了解了一下关于 MCP 的概念,但是想要真正的理解,总是靠看别人的文章,应该没有办法对 mcp 的协议有更深刻的认知。

MCP 的核心概念有三个:

Hosts: 发起连接的 LLM 应用程序,比如 Claude Desktop 或 IDE。

Clients: 在 Hosts 应用程序内部与 Servers 保持一对一的连接。

Servers: 向 Clients 提供上下文、工具和提示。

注:柿子要挑软的捏,骨头要挑硬的啃,那就先从实现一个 MCP Server 开始吧。

2. 动手实现一个 MCP Server

讲真我真的在 google 上查了半天试图找到一个最简单的介绍说明示例,然后照着其步骤动手实践,奈何好像似乎大家写的都过于深奥了,作为一个 MCP Server 的小白,我还是没有办法立刻理解大佬们的思路。

注:知识这个东西,别人总结的总归是别人的,只有自己真的动手实践过的知识才真的是自己的,哈哈哈,当代阿 Q 第一人。

2.1 Why 天气查询

原因简单说明:

  • cursor 是支持直接调用 MCP Server 的,这样就省去自己实现 Hosts 和 clients 的步骤。

  • cursor 的 「@web 」功能已经支持了实时联网查询,可以支持搜索天气,但是毕竟是自己学习理解的过程,选择实现一个查询天气的 MCP Server ,应该不算过分的偷工减料。

注:大模型是不支持实时信息的查询的,cursor 支持是它作为 IDE ,内置集成了很多能力。

请求大模型查询天气的示例:

在这里插入图片描述

2.2 What 天气查询

让我们采用倒序的手法,展示一下两种天气查询的方式的效果:

  • Cursor 内置的 @web 的能力

    在这里插入图片描述

  • 让 cursor 主动调用 mcp server

    在这里插入图片描述

2.3 How 天气查询

2.3.1 配置 MCP Server

配置示例:

{"mcpServers": {"weather": {"type": "sse","url": "http://localhost:8080/sse","description": "查询天气信息","tools": [{"name": "get_weather","description": "获取指定城市的天气信息","parameters": {"city": {"type": "string","description": "城市名称","default": "北京"}}}]}}
}

请求方式:此步骤需要切换请求的新增的 MCP Server

在这里插入图片描述

注:请求的方式随着 cursor 编辑器的迭代和更新可能会有改变,所以以使用 cursor 编辑器版本为准。

笔者在找请求的方式的时候,也是找了很久才找到的,因为网上大部分的文章都是说要使用 MCP:call server

2.3.2 实现 MCP Server

不要过于神话你不理解的知识或者人,其实世界就是一个巨大的草台班子。

极简版本的 MCP Server 只实现了三个 Method:

在这里插入图片描述

完整 go 代码:

package mainimport ("encoding/json""fmt""io""log""net/http""net/url""time""github.com/gin-gonic/gin"
)// 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"`
}func main() {r := gin.Default()// 添加 CORS 中间件r.Use(func(c *gin.Context) {c.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept")c.Header("Access-Control-Expose-Headers", "Content-Type")if c.Request.Method == "OPTIONS" {c.AbortWithStatus(204)return}c.Next()})// 添加 SSE 端点r.GET("/sse", func(c *gin.Context) {log.Printf("收到 SSE 连接请求")// 设置 SSE 相关的 headersc.Header("Content-Type", "text/event-stream")c.Header("Cache-Control", "no-cache")c.Header("Connection", "keep-alive")c.Header("X-Accel-Buffering", "no")c.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Headers", "Content-Type")c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")// 清除之前的任何缓存c.Writer.Flush()// 创建一个通道用于发送心跳ticker := time.NewTicker(30 * time.Second)defer ticker.Stop()// 发送初始连接成功消息err := writeSSEEvent(c.Writer, "ready", MCPResponse{Type: "ready",Content: map[string]interface{}{"status": "connected","tools": []map[string]interface{}{{"name":        "get_weather","description": "获取指定城市的天气信息","parameters": map[string]interface{}{"city": map[string]interface{}{"type":        "string","description": "城市名称","default":     "北京",},},},},},})if err != nil {log.Printf("发送初始消息失败:%v", err)return}// 保持连接并发送心跳for {select {case <-ticker.C:err := writeSSEEvent(c.Writer, "ping", map[string]string{"type": "ping"})if err != nil {log.Printf("发送心跳失败:%v", err)return}case <-c.Request.Context().Done():log.Printf("客户端断开连接")return}}})// 天气查询接口r.POST("/sse/invoke", func(c *gin.Context) {// 设置 CORS 和 SSE 相关的 headersc.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")c.Header("Access-Control-Allow-Headers", "Content-Type")c.Header("Content-Type", "application/json")var request struct {Tool       string                 `json:"tool"`Parameters map[string]interface{} `json:"parameters"`}if err := c.BindJSON(&request); err != nil {c.JSON(http.StatusBadRequest, MCPToolResponse{Name:  request.Tool,Error: fmt.Sprintf("无效的请求格式:%v", err),})return}if request.Tool != "get_weather" {c.JSON(http.StatusBadRequest, MCPToolResponse{Name:  request.Tool,Error: "不支持的工具",})return}city, _ := request.Parameters["city"].(string)if city == "" {city = "北京"}// 构建 wttr.in 的 URLbaseURL := "https://wttr.in/%s?format=j1&lang=zh"encodedCity := url.QueryEscape(city)wttrURL := fmt.Sprintf(baseURL, encodedCity)// 发送请求client := &http.Client{}req, err := http.NewRequest("GET", wttrURL, nil)if err != nil {c.JSON(http.StatusInternalServerError, MCPToolResponse{Name:  request.Tool,Error: fmt.Sprintf("创建请求失败:%v", err),})return}req.Header.Set("User-Agent", "curl/7.64.1")resp, err := client.Do(req)if err != nil {c.JSON(http.StatusInternalServerError, MCPToolResponse{Name:  request.Tool,Error: fmt.Sprintf("获取天气数据失败:%v", err),})return}defer resp.Body.Close()body, err := io.ReadAll(resp.Body)if err != nil {c.JSON(http.StatusInternalServerError, MCPToolResponse{Name:  request.Tool,Error: fmt.Sprintf("读取响应失败:%v", err),})return}var weatherData interface{}if err := json.Unmarshal(body, &weatherData); err != nil {c.JSON(http.StatusInternalServerError, MCPToolResponse{Name:  request.Tool,Error: fmt.Sprintf("解析天气数据失败:%v", err),})return}c.JSON(http.StatusOK, MCPToolResponse{Name:       request.Tool,Parameters: request.Parameters,Response:   weatherData,})})// 添加健康检查接口r.GET("/health", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"status": "ok",})})log.Printf("服务器启动在 http://localhost:8080")r.Run(":8080")
}func writeSSEEvent(w http.ResponseWriter, event string, data interface{}) error {jsonData, err := json.Marshal(data)if err != nil {log.Printf("序列化 SSE 数据失败:%v", err)return fmt.Errorf("序列化 SSE 数据失败:%v", err)}log.Printf("发送 SSE 事件:%s,数据:%s", event, string(jsonData))if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, jsonData); err != nil {log.Printf("写入 SSE 数据失败:%v", err)return fmt.Errorf("写入 SSE 数据失败:%v", err)}if f, ok := w.(http.Flusher); ok {f.Flush()} else {log.Printf("警告:ResponseWriter 不支持 Flush")}return nil
}

3. 碎碎念

发现中午的吃饭的宝藏时间,不仅可以用于锻炼还可以用来快速学习知识,真的是无比开心。Hosts 和 Clients 的实现和理解就交给后面的空余时间啦!

  • 我是一个经常笑的人,可我不是一个经常开心的人。

  • 有些爱好,如果喜欢,哪怕财力上有些吃力,也要尽可能多去体验,不要想着等有钱了再玩儿,大概率是你有钱了,但是爱好也消失了。原因很简单,没有人能逃得过熵增定律,也就是,随着时间的流逝,人生都会越来越复杂,越来越混乱,参数会越来越多,你调参的可能性越来越低,虽然是系统的总能量不变,但其中可用部分减少,哪怕是刚开始是各种不同的人生起点,但熵增到一定程度,其混乱混沌的程度看起来就都差不多,你一开始是你,但最后你和其他人也没什么区别,各种意义上的。世间最让人绝望的无非是:没心情了。

  • 👆🏻 上面这句话真的让我醍醐灌顶了,想学的跳舞要抓紧行动起来了。

4. 参考资料

  • Introduction - Model Context Protocol

  • Example Servers - Model Context Protocol

  • Core architecture - Model Context Protocol

http://www.xdnf.cn/news/362089.html

相关文章:

  • 一文掌握 LVGL 9 的源码目录结构
  • uniapp跨平台开发HarmonyOS NEXT应用初体验
  • 高级可视化图表分析实践——以《大侠立志传》武器系统为例
  • 经典计算核心问题在于多项式时间内无法求解
  • 「Mac畅玩AIGC与多模态27」开发篇23 - 多任务摘要合成与提醒工作流示例
  • Java中的包装类
  • 量化学习DAY2-开始批量提交alpha!
  • 架构师在技术公司中的角色与价值创造
  • Linux ifconfig命令详解
  • Git回顾
  • 服务器不备案有影响吗
  • 依赖关系-根据依赖关系求候选码
  • 智慧校园安全可视化指挥调度系统解决方案
  • 【源码+论文】基于Vue3的企业后台管理系统设计与实现
  • Excel提取单元格特定符号左右两边内容
  • 损失函数(平方损失MSE、绝对值损失MAE、负对数似然损失NLL、交叉熵损失CEL和二元交叉熵损失BCE)原理、公式调库实现与手动实现
  • ISP流程介绍(Raw格式阶段)
  • 模板引用、组件基础
  • 打破虚拟与现实边界,赵伟辰Holo HK项目引领全球用户体验设计革新
  • 跟我学C++中级篇——STL容器的查找对比
  • [AI Tools] Dify 工具插件上传指南:如何将插件发布到官方市场
  • 软件测试的概念
  • 使用Kotlin Flow实现Android应用的响应式编程
  • MTB图像配准算法实现
  • Swagger 3.0 中注解详细示例
  • Linux云计算训练营笔记day05(Rocky Linux中的命令:管道操作 |、wc、find、vim)
  • linux mcelog inject注入
  • 21.第二阶段x64游戏实战-分析采集物偏移
  • C语言printf使用错误导致程序崩溃
  • 39-算法打卡-二叉树-基础知识-第三十九天