272 lines
8.0 KiB
Markdown
272 lines
8.0 KiB
Markdown
---
|
||
title: Go-MCP 接入 Gin:从零搭建 MCP 服务器
|
||
published: 2026-06-01T00:50:00
|
||
updated: 2026-06-01
|
||
description: 介绍如何使用 mark3labs/mcp-go 与 Gin 框架集成,搭建一个带鉴权的 MCP(Model Context Protocol)服务器,让 AI Agent 能直接调用你的后端服务。
|
||
image: /posts/go-mcp-gin/img/cover.svg
|
||
tags: ['Go', 'MCP', 'Gin', 'AI Agent', '后端']
|
||
series: Go 工程实践
|
||
seriesOrder: 1
|
||
difficulty: 进阶
|
||
---
|
||
|
||
## 什么是 MCP
|
||
|
||
MCP(Model Context Protocol)是 Anthropic 提出的开放协议,用于让 AI 助手(如 Claude)与外部服务进行标准化交互。简单来说:你提供一个 MCP 服务器,AI 就能发现并调用你定义的工具(Tools),就像给 AI 装了一双手。
|
||
|
||
传统做法是写 REST API 然后让 AI 通过函数调用(Function Calling)间接使用,但 MCP 统一了协议层,AI 客户端只需要配置一个 URL 就能接入所有工具。
|
||
|
||
## 为什么选 Gin + mcp-go
|
||
|
||
Go 生态里做 MCP 服务器最成熟的库是 `mark3labs/mcp-go`,它支持 StreamableHTTP 传输模式,可以直接挂载到任意 HTTP 框架上。Gin 作为 Go 最流行的 HTTP 框架,天然适配。
|
||
|
||
核心思路:**MCP 服务器本质上就是一个 HTTP Handler,挂到 Gin 路由上即可。**
|
||
|
||
## 需求分析
|
||
|
||
我们要搭建一个具备以下能力的 MCP 服务器:
|
||
|
||
1. **工具注册** — 向 AI 暴露可调用的工具(列出资源、查看详情、创建/删除)
|
||
2. **HTTP 集成** — MCP 端点作为 Gin 路由的一部分,与业务 API 共存
|
||
3. **鉴权** — MCP 请求必须经过身份验证,工具处理器能获取当前用户信息
|
||
|
||
## 实现步骤
|
||
|
||
### 1. 安装依赖
|
||
|
||
```bash
|
||
go get github.com/mark3labs/mcp-go@v0.54.0
|
||
go get github.com/gin-gonic/gin@v1.12.0
|
||
```
|
||
|
||
### 2. 创建 MCP 服务器
|
||
|
||
```go
|
||
import "github.com/mark3labs/mcp-go/server"
|
||
|
||
mcpServer := server.NewMCPServer(
|
||
"my-app", // 服务器名称
|
||
"1.0.0", // 版本
|
||
server.WithLogging(),
|
||
server.WithRecovery(),
|
||
server.WithInstructions("这是一个 MCP 服务器示例"),
|
||
)
|
||
```
|
||
|
||
`NewMCPServer` 创建一个 MCP 协议实例,它负责处理工具发现(`tools/list`)和工具调用(`tools/call`)的协议逻辑。
|
||
|
||
### 3. 注册工具
|
||
|
||
```go
|
||
import "github.com/mark3labs/mcp-go/mcp"
|
||
|
||
// 定义工具
|
||
listTool := mcp.NewTool("list_items",
|
||
mcp.WithDescription("列出所有项目"),
|
||
)
|
||
|
||
// 注册处理器
|
||
mcpServer.AddTool(listTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
// 业务逻辑
|
||
return mcp.NewToolResultText("返回结果"), nil
|
||
})
|
||
```
|
||
|
||
带参数的工具:
|
||
|
||
```go
|
||
getTool := mcp.NewTool("get_item",
|
||
mcp.WithDescription("获取指定项目的详情"),
|
||
mcp.WithString("item_id",
|
||
mcp.Required(),
|
||
mcp.Description("项目 ID"),
|
||
),
|
||
)
|
||
|
||
mcpServer.AddTool(getTool, func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
itemID, err := req.RequireString("item_id")
|
||
if err != nil {
|
||
return mcp.NewToolResultError("缺少 item_id 参数"), nil
|
||
}
|
||
// 查询并返回
|
||
return mcp.NewToolResultText("项目详情..."), nil
|
||
})
|
||
```
|
||
|
||
### 4. 挂载到 Gin 路由
|
||
|
||
关键一步:用 `server.NewStreamableHTTPServer` 把 MCP 实例转成 HTTP Handler,再用 `gin.WrapH` 适配 Gin。
|
||
|
||
```go
|
||
import "github.com/mark3labs/mcp-go/server"
|
||
|
||
mcpHTTPServer := server.NewStreamableHTTPServer(mcpServer,
|
||
server.WithEndpointPath("/mcp"),
|
||
server.WithHeartbeatInterval(30*time.Second),
|
||
)
|
||
|
||
// 挂载到 Gin
|
||
r.Any("/mcp", gin.WrapH(mcpHTTPServer))
|
||
```
|
||
|
||
就这样,`POST /mcp` 端点就生效了。AI 客户端通过这个 URL 发送 JSON-RPC 请求,MCP 服务器自动处理协议握手、工具列表返回、工具调用路由。
|
||
|
||
### 5. 完整的服务启动代码
|
||
|
||
```go
|
||
func main() {
|
||
r := gin.Default()
|
||
|
||
// 创建 MCP 服务器
|
||
mcpServer := server.NewMCPServer("my-app", "1.0.0",
|
||
server.WithLogging(),
|
||
server.WithRecovery(),
|
||
)
|
||
|
||
// 注册工具
|
||
registerTools(mcpServer)
|
||
|
||
// 挂载到 Gin
|
||
mcpHTTP := server.NewStreamableHTTPServer(mcpServer,
|
||
server.WithEndpointPath("/mcp"),
|
||
server.WithHeartbeatInterval(30*time.Second),
|
||
)
|
||
r.Any("/mcp", gin.WrapH(mcpHTTP))
|
||
|
||
// 其他业务路由
|
||
r.GET("/api/health", func(c *gin.Context) {
|
||
c.JSON(200, gin.H{"status": "ok"})
|
||
})
|
||
|
||
r.Run(":8080")
|
||
}
|
||
```
|
||
|
||
此时启动服务,用 curl 测试:
|
||
|
||
```bash
|
||
# 发现工具列表
|
||
curl -X POST http://localhost:8080/mcp \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
||
|
||
# 调用工具
|
||
curl -X POST http://localhost:8080/mcp \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_items","arguments":{}}}'
|
||
```
|
||
|
||
## 拓展:接入鉴权
|
||
|
||
裸奔的 MCP 端点任何人都能调用,生产环境必须加鉴权。思路和普通 API 鉴权一致:Bearer Token。
|
||
|
||
### 问题
|
||
|
||
MCP 的工具处理器签名为 `func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)`,它不接收 Gin 的 `*gin.Context`,所以无法直接在工具处理器里读取请求头。
|
||
|
||
### 解决方案:中间件传递用户信息
|
||
|
||
用两层中间件:第一层做 Gin 的 Token 验证,第二层把用户信息从 Gin Context 复制到 `request.Context`。
|
||
|
||
```go
|
||
// Gin 鉴权中间件
|
||
func AuthRequired(authService *AuthService) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
authHeader := c.GetHeader("Authorization")
|
||
if authHeader == "" {
|
||
c.AbortWithStatusJSON(401, gin.H{"message": "缺少认证令牌"})
|
||
return
|
||
}
|
||
|
||
parts := strings.SplitN(authHeader, " ", 2)
|
||
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
|
||
c.AbortWithStatusJSON(401, gin.H{"message": "格式错误,请使用 Bearer <token>"})
|
||
return
|
||
}
|
||
|
||
user, err := authService.GetUserByToken(parts[1])
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(401, gin.H{"message": "无效的认证令牌"})
|
||
return
|
||
}
|
||
|
||
c.Set("userID", user.ID)
|
||
c.Set("username", user.Username)
|
||
c.Next()
|
||
}
|
||
}
|
||
```
|
||
|
||
```go
|
||
// 用户信息桥接中间件:Gin Context → request.Context
|
||
type ctxKey string
|
||
const (
|
||
ctxUserID ctxKey = "userID"
|
||
ctxUsername ctxKey = "username"
|
||
)
|
||
|
||
func WithUserContext() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
ctx := c.Request.Context()
|
||
ctx = context.WithValue(ctx, ctxUserID, c.GetUint("userID"))
|
||
ctx = context.WithValue(ctx, ctxUsername, c.GetString("username"))
|
||
c.Request = c.Request.WithContext(ctx)
|
||
c.Next()
|
||
}
|
||
}
|
||
```
|
||
|
||
### 组合使用
|
||
|
||
```go
|
||
r.Any("/mcp",
|
||
AuthRequired(authService), // 1. 验证 Token
|
||
WithUserContext(), // 2. 桥接用户信息到 request.Context
|
||
gin.WrapH(mcpHTTP), // 3. MCP 处理
|
||
)
|
||
```
|
||
|
||
在工具处理器里就能拿到用户信息了:
|
||
|
||
```go
|
||
func (h *Handler) handleListItems(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
userID, ok := ctx.Value(ctxUserID).(uint)
|
||
if !ok {
|
||
return mcp.NewToolResultError("未认证"), nil
|
||
}
|
||
|
||
items, _ := h.service.ListByUser(userID)
|
||
// ...
|
||
return mcp.NewToolResultText(result), nil
|
||
}
|
||
```
|
||
|
||
### 客户端配置
|
||
|
||
AI 客户端(如 Claude Desktop)配置 MCP 服务器时带上 Authorization 头:
|
||
|
||
```json
|
||
{
|
||
"mcpServers": {
|
||
"my-app": {
|
||
"url": "http://localhost:8080/mcp",
|
||
"headers": {
|
||
"Authorization": "Bearer your-token-here"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 总结
|
||
|
||
| 步骤 | 说明 |
|
||
|------|------|
|
||
| `NewMCPServer` | 创建 MCP 协议实例 |
|
||
| `NewTool` + `AddTool` | 定义并注册工具 |
|
||
| `NewStreamableHTTPServer` | 转为 HTTP Handler |
|
||
| `gin.WrapH` | 挂载到 Gin 路由 |
|
||
| `AuthRequired` + `WithUserContext` | 鉴权 + 用户信息桥接 |
|
||
|
||
核心就一句话:**MCP 是 HTTP Handler,挂上去就行。** 鉴权复用你已有的中间件体系,只需要多写一个桥接函数把用户信息传进 `request.Context`。
|