@@ -0,0 +1,930 @@
package handlers
import (
cryptoRand "crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"tonav-go/database"
"tonav-go/models"
"github.com/gin-gonic/gin"
)
// ============ MCP (Model Context Protocol) v2.1 ============
// 传输: Streamable HTTP (POST /mcp) + SSE (GET /mcp), JSON-RPC 2.0
// 鉴权: Authorization: Bearer <token>;开关与 Token 存于 settings 表(后台 UI 管理),动态读取即时生效
// getMCPAuthState 从数据库读取 MCP 开关与 Token(每次请求动态读,改动即时生效)
func getMCPAuthState ( ) ( enabled bool , token string ) {
var enabledSetting , tokenSetting models . Setting
database . DB . Where ( "key = ?" , "mcp_enabled" ) . First ( & enabledSetting )
database . DB . Where ( "key = ?" , "mcp_token" ) . First ( & tokenSetting )
return enabledSetting . Value == "true" , tokenSetting . Value
}
// SetupMCP 初始化 MCP 配置(启动时调用:把环境变量 Token 迁移进数据库作为初始值)
func SetupMCP ( ) {
var s models . Setting
if err := database . DB . Where ( "key = ?" , "mcp_token" ) . First ( & s ) . Error ; err != nil {
// 数据库无 token:用环境变量值初始化(兼容旧部署),没有则留空(后台生成)
database . DB . Save ( & models . Setting { Key : "mcp_token" , Value : os . Getenv ( "TONAV_MCP_TOKEN" ) } )
}
if err := database . DB . Where ( "key = ?" , "mcp_enabled" ) . First ( & s ) . Error ; err != nil {
var tok models . Setting
database . DB . Where ( "key = ?" , "mcp_token" ) . First ( & tok )
v := "false"
if tok . Value != "" {
v = "true"
}
database . DB . Save ( & models . Setting { Key : "mcp_enabled" , Value : v } )
}
}
// MCPAuthRequired MCP Bearer Token 鉴权(动态读数据库:后台改开关/Token 即时生效,无需重启)
func MCPAuthRequired ( ) gin . HandlerFunc {
return func ( c * gin . Context ) {
enabled , token := getMCPAuthState ( )
if ! enabled || token == "" {
c . AbortWithStatusJSON ( http . StatusServiceUnavailable , gin . H {
"jsonrpc" : "2.0" ,
"error" : gin . H {
"code" : - 32000 ,
"message" : "MCP 未启用:请在后台设置中开启 MCP Server 并生成 Token" ,
} ,
"id" : nil ,
} )
return
}
auth := c . GetHeader ( "Authorization" )
tok := strings . TrimPrefix ( auth , "Bearer " )
if tok == "" || tok != token {
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_search_services" ,
Description : "全文搜索 ToNav 服务:按关键词匹配名称/URL/描述/标签,返回按相关度排序的结果。比 tonav_list_services 更适合查找特定服务。" ,
InputSchema : map [ string ] interface { } {
"type" : "object" ,
"properties" : map [ string ] interface { } {
"query" : map [ string ] interface { } { "type" : "string" , "description" : "搜索关键词,必填" } ,
"tag" : map [ string ] interface { } { "type" : "string" , "description" : "可选:按标签收窄结果" } ,
} ,
"required" : [ ] string { "query" } ,
} ,
} ,
{
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 )
}
notifySSE ( "message" , map [ string ] interface { } {
"jsonrpc" : "2.0" ,
"method" : "notifications/tools/list_changed" ,
} )
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 )
}
notifySSE ( "message" , map [ string ] interface { } {
"jsonrpc" : "2.0" ,
"method" : "notifications/tools/list_changed" ,
} )
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
}
// toolSearchServices 全文搜索服务(v2 独立搜索工具)
func toolSearchServices ( args map [ string ] interface { } ) ( interface { } , error ) {
query , _ := args [ "query" ] . ( string )
query = strings . TrimSpace ( query )
if query == "" {
return nil , fmt . Errorf ( "query 不能为空" )
}
var svcs [ ] models . Service
q := database . DB . Order ( "sort_order desc" )
if tag , _ := args [ "tag" ] . ( string ) ; strings . TrimSpace ( tag ) != "" {
q = q . Where ( "tags LIKE ?" , "%" + strings . TrimSpace ( tag ) + "%" )
}
like := "%" + query + "%"
q = q . Where (
database . DB . Where ( "name LIKE ?" , like ) . Or ( "url LIKE ?" , like ) .
Or ( "description LIKE ?" , like ) . Or ( "tags LIKE ?" , like ) ,
)
q . Find ( & svcs )
// 计算简单相关度评分:名称匹配 > 标签匹配 > 描述匹配 > URL 匹配
type scored struct {
models . Service
Score int ` json:"score" `
Match string ` json:"matched_field" `
}
results := make ( [ ] scored , 0 , len ( svcs ) )
for _ , s := range svcs {
sc := scored { Service : s }
ql := strings . ToLower ( query )
switch {
case strings . Contains ( strings . ToLower ( s . Name ) , ql ) :
sc . Score , sc . Match = 4 , "name"
case strings . Contains ( strings . ToLower ( s . Tags ) , ql ) :
sc . Score , sc . Match = 3 , "tags"
case strings . Contains ( strings . ToLower ( s . Description ) , ql ) :
sc . Score , sc . Match = 2 , "description"
default :
sc . Score , sc . Match = 1 , "url"
}
results = append ( results , sc )
}
for i := 0 ; i < len ( results ) ; i ++ {
for j := i + 1 ; j < len ( results ) ; j ++ {
if results [ j ] . Score > results [ i ] . Score {
results [ i ] , results [ j ] = results [ j ] , results [ i ]
}
}
}
return map [ string ] interface { } {
"query" : query ,
"count" : len ( results ) ,
"services" : results ,
} , 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 )
}
notifySSE ( "message" , map [ string ] interface { } {
"jsonrpc" : "2.0" ,
"method" : "notifications/tools/list_changed" ,
} )
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_search_services" :
return toolSearchServices ( 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 ) {
enabled , token := getMCPAuthState ( )
if ! enabled || token == "" {
c . JSON ( http . StatusServiceUnavailable , mcpError ( nil , - 32000 , "MCP 未启用:请在后台设置中开启" ) )
return
}
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" : "2.1.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 , & params ) ; 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 )
}
}
// ============ 后台 MCP 管理 API (v2.1) ============
// MCPStatus 后台查询 MCP 状态(Token 掩码显示)
func MCPStatus ( c * gin . Context ) {
enabled , token := getMCPAuthState ( )
masked := ""
if token != "" {
if len ( token ) > 10 {
masked = token [ : 6 ] + "…" + token [ len ( token ) - 4 : ]
} else {
masked = "****"
}
}
c . JSON ( http . StatusOK , gin . H {
"enabled" : enabled ,
"has_token" : token != "" ,
"token_masked" : masked ,
} )
}
// MCPRegenerateToken 生成新随机 Token 并自动启用 MCP,返回完整新 Token(仅此一次明文展示)
func MCPRegenerateToken ( c * gin . Context ) {
b := make ( [ ] byte , 24 )
if _ , err := cryptoRand . Read ( b ) ; err != nil {
c . JSON ( http . StatusInternalServerError , Response { Success : false , Message : "生成失败: " + err . Error ( ) } )
return
}
token := hex . EncodeToString ( b )
database . DB . Save ( & models . Setting { Key : "mcp_token" , Value : token } )
database . DB . Save ( & models . Setting { Key : "mcp_enabled" , Value : "true" } )
c . JSON ( http . StatusOK , gin . H { "success" : true , "token" : token , "message" : "已生成新 Token 并启用 MCP" } )
}
// MCPSetEnabled 开启/关闭 MCP( Token 为空时拒绝开启)
func MCPSetEnabled ( c * gin . Context ) {
var input struct {
Enabled bool ` json:"enabled" `
}
if err := c . ShouldBindJSON ( & input ) ; err != nil {
c . JSON ( http . StatusBadRequest , Response { Success : false , Message : err . Error ( ) } )
return
}
if input . Enabled {
_ , token := getMCPAuthState ( )
if token == "" {
c . JSON ( http . StatusBadRequest , Response { Success : false , Message : "Token 为空,请先生成 Token" } )
return
}
}
database . DB . Save ( & models . Setting { Key : "mcp_enabled" , Value : strconv . FormatBool ( input . Enabled ) } )
msg := "MCP 已关闭"
if input . Enabled {
msg = "MCP 已开启,即时生效"
}
c . JSON ( http . StatusOK , Response { Success : true , Message : msg } )
}
// GetMCPHeaderToken 供测试使用
func GetMCPHeaderToken ( ) string {
_ , token := getMCPAuthState ( )
return token
}
// ============ SSE 端点 (GET /mcp, v2) ============
// mcpEventCh SSE 事件广播通道(多客户端)
var (
sseMu sync . Mutex
sseClients = make ( map [ chan sseEvent ] struct { } )
)
type sseEvent struct {
Event string
Data interface { }
}
// notifySSE 向所有已连接 SSE 客户端广播一条通知(非阻塞)
func notifySSE ( event string , data interface { } ) {
sseMu . Lock ( )
defer sseMu . Unlock ( )
for ch := range sseClients {
select {
case ch <- sseEvent { Event : event , Data : data } :
default : // 慢客户端直接丢弃,不阻塞
}
}
}
// MCPSSEHandler 处理 GET /mcp (SSE 流式端点)
// 客户端可收到: endpoint(初始化握手)、message(服务端主动通知)等事件
func MCPSSEHandler ( c * gin . Context ) {
enabled , token := getMCPAuthState ( )
if ! enabled || token == "" {
c . JSON ( http . StatusServiceUnavailable , mcpError ( nil , - 32000 , "MCP 未启用:请在后台设置中开启" ) )
return
}
c . Writer . Header ( ) . Set ( "Content-Type" , "text/event-stream" )
c . Writer . Header ( ) . Set ( "Cache-Control" , "no-cache" )
c . Writer . Header ( ) . Set ( "Connection" , "keep-alive" )
c . Writer . Header ( ) . Set ( "X-Accel-Buffering" , "no" )
c . Writer . WriteHeader ( http . StatusOK )
ch := make ( chan sseEvent , 16 )
sseMu . Lock ( )
sseClients [ ch ] = struct { } { }
sseMu . Unlock ( )
defer func ( ) {
sseMu . Lock ( )
delete ( sseClients , ch )
sseMu . Unlock ( )
} ( )
// 初始 endpoint 事件:告知客户端 POST 端点地址
writeSSE ( c , "endpoint" , "/mcp" )
c . Writer . Flush ( )
ticker := time . NewTicker ( 30 * time . Second ) // keepalive 注释行
defer ticker . Stop ( )
for {
select {
case ev := <- ch :
writeSSE ( c , ev . Event , ev . Data )
c . Writer . Flush ( )
case <- ticker . C :
// SSE 注释行作为 keepalive
_ , _ = c . Writer . WriteString ( ": keepalive\n\n" )
c . Writer . Flush ( )
case <- c . Request . Context ( ) . Done ( ) :
return
}
}
}
func writeSSE ( c * gin . Context , event string , data interface { } ) {
dataBytes , err := json . Marshal ( data )
if err != nil {
return
}
fmt . Fprintf ( c . Writer , "event: %s\ndata: %s\n\n" , event , dataBytes )
}
// MCPNotificationSender 供业务代码在服务/分类变更时推送通知
// 规范:notifications/tools/list_changed
func NotifyToolsChanged ( ) {
notifySSE ( "message" , map [ string ] interface { } {
"jsonrpc" : "2.0" ,
"method" : "notifications/tools/list_changed" ,
} )
}