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 main
import (
"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 相关的 headers
c.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 相关的 headers
c.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 的 URL
baseURL := "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 的实现和理解就交给后面的空余时间啦!
- 我是一个经常笑的人,可我不是一个经常开心的人。
- 有些爱好,如果喜欢,哪怕财力上有些吃力,也要尽可能多去体验,不要想着等有钱了再玩儿,大概率是你有钱了,但是爱好也消失了。原因很简单,没有人能逃得过熵增定律,也就是,随着时间的流逝,人生都会越来越复杂,越来越混乱,参数会越来越多,你调参的可能性越来越低,虽然是系统的总能量不变,但其中可用部分减少,哪怕是刚开始是各种不同的人生起点,但熵增到一定程度,其混乱混沌的程度看起来就都差不多,你一开始是你,但最后你和其他人也没什么区别,各种意义上的。世间最让人绝望的无非是:没心情了。
- 👆🏻 上面这句话真的让我醍醐灌顶了,想学的跳舞要抓紧行动起来了。