让 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