feat: 原生集成 MCP Server (POST /mcp, JSON-RPC 2.0, Bearer Token)
- Streamable HTTP 传输,支持 initialize/ping/tools/list/tools/call 及批量调用 - 6 个工具: add_service(自动抓 title/description/favicon)、create_category、 list_categories(含服务计数)、list_services(keyword/category/tag 过滤)、 update_service、delete_service - 鉴权: TONAV_MCP_TOKEN 环境变量 (systemd 注入)
This commit is contained in:
+665
@@ -0,0 +1,665 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"tonav-go/database"
|
||||
"tonav-go/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ============ MCP (Model Context Protocol) v1 ============
|
||||
// 传输: Streamable HTTP (POST /mcp, JSON-RPC 2.0)
|
||||
// 鉴权: Authorization: Bearer <TONAV_MCP_TOKEN>
|
||||
|
||||
type mcpConfig struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
var mcpToken string
|
||||
|
||||
// LoadMCPConfig 读取 MCP 配置(环境变量 TONAV_MCP_TOKEN)
|
||||
func LoadMCPConfig() {
|
||||
mcpToken = os.Getenv("TONAV_MCP_TOKEN")
|
||||
if mcpToken == "" {
|
||||
mcpToken = "tonav-mcp-default-token"
|
||||
}
|
||||
}
|
||||
|
||||
// MCPAuthRequired MCP Bearer Token 鉴权
|
||||
func MCPAuthRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
auth := c.GetHeader("Authorization")
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
if token == "" || token != mcpToken {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"jsonrpc": "2.0",
|
||||
"error": gin.H{
|
||||
"code": -32001,
|
||||
"message": "unauthorized: invalid or missing bearer token",
|
||||
},
|
||||
"id": nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ============ JSON-RPC 结构 ============
|
||||
|
||||
type jsonRPCRequest struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID json.RawMessage `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
type jsonRPCResponse struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID interface{} `json:"id"`
|
||||
Result interface{} `json:"result,omitempty"`
|
||||
Error *jsonRPCError `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type jsonRPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// ============ MCP 工具定义 ============
|
||||
|
||||
type mcpTool struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema map[string]interface{} `json:"inputSchema"`
|
||||
}
|
||||
|
||||
func mcpToolDefs() []mcpTool {
|
||||
return []mcpTool{
|
||||
{
|
||||
Name: "tonav_add_service",
|
||||
Description: "向 ToNav 导航页添加一个服务网址。可指定名称、分类(不存在则自动创建)、标签、描述、是否开启健康检查。不传 name 时会尝试抓取目标网页的 <title> 自动命名。",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{"type": "string", "description": "服务网址,必填"},
|
||||
"name": map[string]interface{}{"type": "string", "description": "服务名称,不填自动抓取网页 title"},
|
||||
"category": map[string]interface{}{"type": "string", "description": "分类名称,不存在时自动创建"},
|
||||
"tags": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "标签列表,如 [\"监控\",\"告警\"]"},
|
||||
"description": map[string]interface{}{"type": "string", "description": "服务描述,不填自动抓取网页 meta description"},
|
||||
"icon": map[string]interface{}{"type": "string", "description": "图标 URL,不填自动使用 /favicon.ico"},
|
||||
"health_check_enabled": map[string]interface{}{"type": "boolean", "description": "是否开启健康检查,默认 true"},
|
||||
"health_check_url": map[string]interface{}{"type": "string", "description": "健康检查地址,默认同 url"},
|
||||
"is_enabled": map[string]interface{}{"type": "boolean", "description": "是否启用显示,默认 true"},
|
||||
},
|
||||
"required": []string{"url"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tonav_create_category",
|
||||
Description: "在 ToNav 中创建一个分类",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"name": map[string]interface{}{"type": "string", "description": "分类名称,必填"},
|
||||
"sort_order": map[string]interface{}{"type": "integer", "description": "排序权重,数字越大越靠前,默认 0"},
|
||||
},
|
||||
"required": []string{"name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tonav_list_categories",
|
||||
Description: "列出 ToNav 所有分类(含每个分类下的服务数量)",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tonav_list_services",
|
||||
Description: "列出 ToNav 服务,可按关键词/分类名/标签过滤",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"keyword": map[string]interface{}{"type": "string", "description": "按名称/URL/描述模糊匹配"},
|
||||
"category": map[string]interface{}{"type": "string", "description": "按分类名过滤"},
|
||||
"tag": map[string]interface{}{"type": "string", "description": "按标签过滤"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tonav_update_service",
|
||||
Description: "按名称或 ID 更新已有服务:可改名称/URL/分类/标签/描述/启用状态/健康检查开关",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"id": map[string]interface{}{"type": "integer", "description": "服务 ID(与 name 二选一)"},
|
||||
"name": map[string]interface{}{"type": "string", "description": "现有服务名称(用于定位服务)"},
|
||||
"new_name": map[string]interface{}{"type": "string", "description": "新名称"},
|
||||
"url": map[string]interface{}{"type": "string", "description": "新 URL"},
|
||||
"category": map[string]interface{}{"type": "string", "description": "新分类名(不存在自动创建)"},
|
||||
"tags": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "新标签列表(整体替换)"},
|
||||
"description": map[string]interface{}{"type": "string", "description": "新描述"},
|
||||
"is_enabled": map[string]interface{}{"type": "boolean"},
|
||||
"health_check_enabled": map[string]interface{}{"type": "boolean"},
|
||||
"health_check_url": map[string]interface{}{"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tonav_delete_service",
|
||||
Description: "按名称或 ID 删除一个服务",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"id": map[string]interface{}{"type": "integer", "description": "服务 ID(与 name 二选一)"},
|
||||
"name": map[string]interface{}{"type": "string", "description": "服务名称"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 工具实现 ============
|
||||
|
||||
// fetchPageMeta 抓取网页 <title> 与 meta description
|
||||
func fetchPageMeta(rawURL string) (title, desc string) {
|
||||
client := &http.Client{Timeout: 6 * time.Second}
|
||||
if !strings.HasPrefix(rawURL, "http") {
|
||||
rawURL = "https://" + rawURL
|
||||
}
|
||||
resp, err := client.Get(rawURL)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
html := string(body)
|
||||
|
||||
// title
|
||||
if s := strings.Index(strings.ToLower(html), "<title"); s >= 0 {
|
||||
rest := html[s:]
|
||||
if e := strings.Index(strings.ToLower(rest), "</title>"); e >= 0 {
|
||||
inner := rest[len("<title"):e]
|
||||
if c := strings.Index(inner, ">"); c >= 0 {
|
||||
title = strings.TrimSpace(inner[c+1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
// meta description
|
||||
lower := strings.ToLower(html)
|
||||
if idx := strings.Index(lower, "name=\"description\""); idx >= 0 {
|
||||
seg := html[idx:]
|
||||
if cIdx := strings.Index(strings.ToLower(seg), "content=\""); cIdx >= 0 {
|
||||
seg = seg[cIdx+len("content=\""):]
|
||||
if eIdx := strings.Index(seg, "\""); eIdx >= 0 {
|
||||
desc = strings.TrimSpace(seg[:eIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(title) > 200 {
|
||||
title = title[:200]
|
||||
}
|
||||
if len(desc) > 500 {
|
||||
desc = desc[:500]
|
||||
}
|
||||
return title, desc
|
||||
}
|
||||
|
||||
// normalizeURL 规范化 URL
|
||||
func normalizeURL(u string) string {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" && !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
||||
u = "https://" + u
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// ensureCategory 获取或创建分类,返回分类 ID
|
||||
func ensureCategory(name string) (uint, error) {
|
||||
var cat models.Category
|
||||
if err := database.DB.Where("name = ?", name).First(&cat).Error; err == nil {
|
||||
return cat.ID, nil
|
||||
}
|
||||
cat = models.Category{Name: name, SortOrder: 50}
|
||||
if err := database.DB.Create(&cat).Error; err != nil {
|
||||
// 并发或重名冲突时再查一次
|
||||
if err2 := database.DB.Where("name = ?", name).First(&cat).Error; err2 == nil {
|
||||
return cat.ID, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
return cat.ID, nil
|
||||
}
|
||||
|
||||
// findService 按 ID 或名称定位服务
|
||||
func findService(id float64, name string) (*models.Service, error) {
|
||||
var svc models.Service
|
||||
var err error
|
||||
if id > 0 {
|
||||
err = database.DB.First(&svc, uint(id)).Error
|
||||
} else if name != "" {
|
||||
err = database.DB.Where("name = ?", name).First(&svc).Error
|
||||
} else {
|
||||
err = fmt.Errorf("必须提供 id 或 name")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &svc, nil
|
||||
}
|
||||
|
||||
// tagsToString 标签数组转存储格式(逗号分隔)
|
||||
func tagsToString(tags []interface{}) string {
|
||||
if len(tags) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(tags))
|
||||
for _, t := range tags {
|
||||
if s, ok := t.(string); ok && strings.TrimSpace(s) != "" {
|
||||
parts = append(parts, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
// toolAddService 添加服务
|
||||
func toolAddService(args map[string]interface{}) (interface{}, error) {
|
||||
rawURL, _ := args["url"].(string)
|
||||
if strings.TrimSpace(rawURL) == "" {
|
||||
return nil, fmt.Errorf("url 不能为空")
|
||||
}
|
||||
url := normalizeURL(rawURL)
|
||||
|
||||
name, _ := args["name"].(string)
|
||||
description, _ := args["description"].(string)
|
||||
category, _ := args["category"].(string)
|
||||
|
||||
// 自动抓取 title / description
|
||||
autoTitle, autoDesc := "", ""
|
||||
if strings.TrimSpace(name) == "" || strings.TrimSpace(description) == "" {
|
||||
autoTitle, autoDesc = fetchPageMeta(url)
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
name = autoTitle
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
// 兜底用 host
|
||||
u := strings.TrimPrefix(strings.TrimPrefix(url, "https://"), "http://")
|
||||
name = strings.Split(u, "/")[0]
|
||||
}
|
||||
if strings.TrimSpace(description) == "" {
|
||||
description = autoDesc
|
||||
}
|
||||
|
||||
healthEnabled := true
|
||||
if v, ok := args["health_check_enabled"].(bool); ok {
|
||||
healthEnabled = v
|
||||
}
|
||||
isEnabled := true
|
||||
if v, ok := args["is_enabled"].(bool); ok {
|
||||
isEnabled = v
|
||||
}
|
||||
healthURL, _ := args["health_check_url"].(string)
|
||||
|
||||
var categoryID uint
|
||||
if strings.TrimSpace(category) != "" {
|
||||
cid, err := ensureCategory(strings.TrimSpace(category))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建分类失败: %v", err)
|
||||
}
|
||||
categoryID = cid
|
||||
}
|
||||
|
||||
icon, _ := args["icon"].(string)
|
||||
if strings.TrimSpace(icon) == "" {
|
||||
u := strings.TrimPrefix(strings.TrimPrefix(url, "https://"), "http://")
|
||||
host := strings.Split(u, "/")[0]
|
||||
icon = "https://" + host + "/favicon.ico"
|
||||
}
|
||||
|
||||
// 查重:同名或同 URL 已存在则返回提示
|
||||
var existed models.Service
|
||||
if err := database.DB.Where("url = ?", url).First(&existed).Error; err == nil {
|
||||
return map[string]interface{}{
|
||||
"created": false,
|
||||
"message": fmt.Sprintf("已存在相同 URL 的服务「%s」(id=%d),未重复创建。可使用 tonav_update_service 更新它。", existed.Name, existed.ID),
|
||||
"existing": existed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
svc := models.Service{
|
||||
Name: strings.TrimSpace(name),
|
||||
URL: url,
|
||||
Description: strings.TrimSpace(description),
|
||||
Icon: icon,
|
||||
CategoryID: categoryID,
|
||||
Status: "unknown",
|
||||
Tags: tagsToString(args["tags"].([]interface{})),
|
||||
IsEnabled: isEnabled,
|
||||
HealthCheckURL: strings.TrimSpace(healthURL),
|
||||
HealthCheckEnabled: healthEnabled,
|
||||
}
|
||||
// tags 需要单独处理(args["tags"] 可能不存在)
|
||||
if rawTags, ok := args["tags"]; ok {
|
||||
if arr, ok := rawTags.([]interface{}); ok {
|
||||
svc.Tags = tagsToString(arr)
|
||||
}
|
||||
}
|
||||
if svc.Tags == "" {
|
||||
if arr, ok := args["tags"].([]interface{}); ok {
|
||||
svc.Tags = tagsToString(arr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := database.DB.Create(&svc).Error; err != nil {
|
||||
return nil, fmt.Errorf("创建失败: %v", err)
|
||||
}
|
||||
|
||||
catName := category
|
||||
if catName == "" && categoryID > 0 {
|
||||
var cat models.Category
|
||||
if database.DB.First(&cat, categoryID).Error == nil {
|
||||
catName = cat.Name
|
||||
}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"created": true,
|
||||
"message": fmt.Sprintf("已添加服务「%s」%s", svc.Name, func() string { if catName != "" { return " 到分类「" + catName + "」" }; return "" }()),
|
||||
"service": svc,
|
||||
"category_id": categoryID,
|
||||
"category": catName,
|
||||
"tags": svc.Tags,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toolCreateCategory 创建分类
|
||||
func toolCreateCategory(args map[string]interface{}) (interface{}, error) {
|
||||
name, _ := args["name"].(string)
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, fmt.Errorf("name 不能为空")
|
||||
}
|
||||
sortOrder := 0
|
||||
if v, ok := args["sort_order"].(float64); ok {
|
||||
sortOrder = int(v)
|
||||
}
|
||||
var existed models.Category
|
||||
if err := database.DB.Where("name = ?", name).First(&existed).Error; err == nil {
|
||||
return map[string]interface{}{
|
||||
"created": false,
|
||||
"message": fmt.Sprintf("分类「%s」已存在 (id=%d)", existed.Name, existed.ID),
|
||||
"category": existed,
|
||||
}, nil
|
||||
}
|
||||
cat := models.Category{Name: strings.TrimSpace(name), SortOrder: sortOrder}
|
||||
if err := database.DB.Create(&cat).Error; err != nil {
|
||||
return nil, fmt.Errorf("创建分类失败: %v", err)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"created": true,
|
||||
"message": fmt.Sprintf("已创建分类「%s」", cat.Name),
|
||||
"category": cat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toolListCategories 列出分类
|
||||
func toolListCategories(_ map[string]interface{}) (interface{}, error) {
|
||||
var cats []models.Category
|
||||
database.DB.Order("sort_order desc").Find(&cats)
|
||||
type catWithCount struct {
|
||||
models.Category
|
||||
ServiceCount int64 `json:"service_count"`
|
||||
}
|
||||
result := make([]catWithCount, 0, len(cats))
|
||||
for _, c := range cats {
|
||||
var cnt int64
|
||||
database.DB.Model(&models.Service{}).Where("category_id = ?", c.ID).Count(&cnt)
|
||||
result = append(result, catWithCount{Category: c, ServiceCount: cnt})
|
||||
}
|
||||
return map[string]interface{}{"categories": result}, nil
|
||||
}
|
||||
|
||||
// toolListServices 列出服务
|
||||
func toolListServices(args map[string]interface{}) (interface{}, error) {
|
||||
var svcs []models.Service
|
||||
q := database.DB.Order("category_id asc, sort_order desc")
|
||||
keyword, _ := args["keyword"].(string)
|
||||
category, _ := args["category"].(string)
|
||||
tag, _ := args["tag"].(string)
|
||||
|
||||
if category != "" {
|
||||
var cat models.Category
|
||||
if err := database.DB.Where("name = ?", category).First(&cat).Error; err != nil {
|
||||
return map[string]interface{}{"services": []models.Service{}, "message": "分类不存在: " + category}, nil
|
||||
}
|
||||
q = q.Where("category_id = ?", cat.ID)
|
||||
}
|
||||
if tag != "" {
|
||||
q = q.Where("tags LIKE ?", "%"+tag+"%")
|
||||
}
|
||||
if keyword != "" {
|
||||
like := "%" + keyword + "%"
|
||||
q = q.Where("name LIKE ? OR url LIKE ? OR description LIKE ?", like, like, like)
|
||||
}
|
||||
q.Find(&svcs)
|
||||
return map[string]interface{}{"count": len(svcs), "services": svcs}, nil
|
||||
}
|
||||
|
||||
// toolUpdateService 更新服务
|
||||
func toolUpdateService(args map[string]interface{}) (interface{}, error) {
|
||||
id, _ := args["id"].(float64)
|
||||
name, _ := args["name"].(string)
|
||||
svc, err := findService(id, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("未找到服务: %v", err)
|
||||
}
|
||||
|
||||
changes := map[string]interface{}{}
|
||||
if v, ok := args["new_name"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
changes["name"] = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := args["url"].(string); ok && strings.TrimSpace(v) != "" {
|
||||
changes["url"] = normalizeURL(v)
|
||||
}
|
||||
if v, ok := args["description"].(string); ok {
|
||||
changes["description"] = v
|
||||
}
|
||||
if v, ok := args["health_check_url"].(string); ok {
|
||||
changes["health_check_url"] = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := args["is_enabled"].(bool); ok {
|
||||
changes["is_enabled"] = v
|
||||
}
|
||||
if v, ok := args["health_check_enabled"].(bool); ok {
|
||||
changes["health_check_enabled"] = v
|
||||
}
|
||||
if v, ok := args["tags"]; ok {
|
||||
if arr, ok := v.([]interface{}); ok {
|
||||
changes["tags"] = tagsToString(arr)
|
||||
}
|
||||
}
|
||||
if cat, ok := args["category"].(string); ok && strings.TrimSpace(cat) != "" {
|
||||
cid, err := ensureCategory(strings.TrimSpace(cat))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建分类失败: %v", err)
|
||||
}
|
||||
changes["category_id"] = cid
|
||||
}
|
||||
|
||||
if len(changes) == 0 {
|
||||
return nil, fmt.Errorf("未提供任何需要更新的字段")
|
||||
}
|
||||
if err := database.DB.Model(svc).Updates(changes).Error; err != nil {
|
||||
return nil, fmt.Errorf("更新失败: %v", err)
|
||||
}
|
||||
database.DB.First(svc, svc.ID)
|
||||
return map[string]interface{}{"updated": true, "message": fmt.Sprintf("已更新服务「%s」", svc.Name), "service": svc}, nil
|
||||
}
|
||||
|
||||
// toolDeleteService 删除服务
|
||||
func toolDeleteService(args map[string]interface{}) (interface{}, error) {
|
||||
id, _ := args["id"].(float64)
|
||||
name, _ := args["name"].(string)
|
||||
svc, err := findService(id, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("未找到服务: %v", err)
|
||||
}
|
||||
if err := database.DB.Delete(svc).Error; err != nil {
|
||||
return nil, fmt.Errorf("删除失败: %v", err)
|
||||
}
|
||||
return map[string]interface{}{"deleted": true, "message": fmt.Sprintf("已删除服务「%s」(id=%d)", svc.Name, svc.ID)}, nil
|
||||
}
|
||||
|
||||
// ============ JSON-RPC 处理 ============
|
||||
|
||||
func mcpResult(id json.RawMessage, result interface{}) gin.H {
|
||||
var idVal interface{}
|
||||
if len(id) > 0 {
|
||||
_ = json.Unmarshal(id, &idVal)
|
||||
}
|
||||
return gin.H{"jsonrpc": "2.0", "id": idVal, "result": result}
|
||||
}
|
||||
|
||||
func mcpError(id json.RawMessage, code int, msg string) gin.H {
|
||||
var idVal interface{}
|
||||
if len(id) > 0 {
|
||||
_ = json.Unmarshal(id, &idVal)
|
||||
}
|
||||
return gin.H{"jsonrpc": "2.0", "id": idVal, "error": gin.H{"code": code, "message": msg}}
|
||||
}
|
||||
|
||||
func callTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||
switch name {
|
||||
case "tonav_add_service":
|
||||
return toolAddService(args)
|
||||
case "tonav_create_category":
|
||||
return toolCreateCategory(args)
|
||||
case "tonav_list_categories":
|
||||
return toolListCategories(args)
|
||||
case "tonav_list_services":
|
||||
return toolListServices(args)
|
||||
case "tonav_update_service":
|
||||
return toolUpdateService(args)
|
||||
case "tonav_delete_service":
|
||||
return toolDeleteService(args)
|
||||
default:
|
||||
return nil, fmt.Errorf("未知工具: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// MCPHandler 处理 POST /mcp (Streamable HTTP)
|
||||
func MCPHandler(c *gin.Context) {
|
||||
body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1024*1024))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, mcpError(nil, -32700, "读取请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
// 支持批量调用
|
||||
var batch []jsonRPCRequest
|
||||
var single jsonRPCRequest
|
||||
if err := json.Unmarshal(body, &batch); err == nil && len(batch) > 0 {
|
||||
responses := make([]gin.H, 0, len(batch))
|
||||
for _, req := range batch {
|
||||
if resp := handleMCPRequest(req); resp != nil {
|
||||
responses = append(responses, resp)
|
||||
}
|
||||
}
|
||||
if len(responses) == 0 {
|
||||
c.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, responses)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &single); err != nil {
|
||||
c.JSON(http.StatusBadRequest, mcpError(nil, -32700, "解析错误: "+err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
resp := handleMCPRequest(single)
|
||||
if resp == nil {
|
||||
// notification -> 202 Accepted
|
||||
c.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// handleMCPRequest 处理单条 JSON-RPC 请求;notification 返回 nil
|
||||
func handleMCPRequest(req jsonRPCRequest) gin.H {
|
||||
switch req.Method {
|
||||
case "initialize":
|
||||
proto, _ := strconv.Unquote(string(req.Params))
|
||||
_ = proto
|
||||
return mcpResult(req.ID, map[string]interface{}{
|
||||
"protocolVersion": "2025-03-26",
|
||||
"capabilities": map[string]interface{}{
|
||||
"tools": map[string]interface{}{"listChanged": false},
|
||||
},
|
||||
"serverInfo": map[string]interface{}{
|
||||
"name": "tonav-mcp",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
})
|
||||
|
||||
case "notifications/initialized", "initialized":
|
||||
return nil // notification
|
||||
|
||||
case "ping":
|
||||
return mcpResult(req.ID, map[string]interface{}{})
|
||||
|
||||
case "tools/list":
|
||||
return mcpResult(req.ID, map[string]interface{}{"tools": mcpToolDefs()})
|
||||
|
||||
case "tools/call":
|
||||
var params struct {
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
}
|
||||
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
||||
return mcpError(req.ID, -32602, "参数解析失败: "+err.Error())
|
||||
}
|
||||
if params.Arguments == nil {
|
||||
params.Arguments = map[string]interface{}{}
|
||||
}
|
||||
result, err := callTool(params.Name, params.Arguments)
|
||||
if err != nil {
|
||||
return mcpResult(req.ID, map[string]interface{}{
|
||||
"content": []map[string]interface{}{{
|
||||
"type": "text",
|
||||
"text": "工具执行失败: " + err.Error(),
|
||||
}},
|
||||
"isError": true,
|
||||
})
|
||||
}
|
||||
data, _ := json.Marshal(result)
|
||||
return mcpResult(req.ID, map[string]interface{}{
|
||||
"content": []map[string]interface{}{{
|
||||
"type": "text",
|
||||
"text": string(data),
|
||||
}},
|
||||
})
|
||||
|
||||
default:
|
||||
return mcpError(req.ID, -32601, "未知方法: "+req.Method)
|
||||
}
|
||||
}
|
||||
|
||||
// GetMCPHeaderToken 供测试使用
|
||||
func GetMCPHeaderToken() string {
|
||||
return mcpToken
|
||||
}
|
||||
Reference in New Issue
Block a user