Files
SuBlog/content/posts/go-mcp-gin/index.md
T

272 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Go-MCP 接入 Gin:从零搭建 MCP 服务器
published: 2026-06-01T00:50:00
updated: 2026-06-01
description: 介绍如何使用 mark3labs/mcp-go 与 Gin 框架集成,搭建一个带鉴权的 MCPModel Context Protocol)服务器,让 AI Agent 能直接调用你的后端服务。
image: /posts/go-mcp-gin/img/cover.svg
tags: ['Go', 'MCP', 'Gin', 'AI Agent', '后端']
series: Go 工程实践
seriesOrder: 1
difficulty: 进阶
---
## 什么是 MCP
MCPModel 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`