refactor: 迁移到官方 MCP SDK (#167)
- 添加官方 SDK 依赖 github.com/modelcontextprotocol/go-sdk v0.7.0 - 新增 mcp_server.go 使用官方 SDK 注册 8 个 MCP 工具 - 删除自实现的 streamable_http.go(约 400 行) - 更新 routes.go 使用 mcp.NewStreamableHTTPHandler - 优化服务器优雅关闭逻辑(5秒超时 + 警告日志) - 清理 types.go 中的 JSON-RPC 相关类型 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -9,21 +9,28 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppServer 应用服务器结构体,封装所有服务和处理器
|
// AppServer 应用服务器结构体,封装所有服务和处理器
|
||||||
type AppServer struct {
|
type AppServer struct {
|
||||||
xiaohongshuService *XiaohongshuService
|
xiaohongshuService *XiaohongshuService
|
||||||
|
mcpServer *mcp.Server
|
||||||
router *gin.Engine
|
router *gin.Engine
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAppServer 创建新的应用服务器实例
|
// NewAppServer 创建新的应用服务器实例
|
||||||
func NewAppServer(xiaohongshuService *XiaohongshuService) *AppServer {
|
func NewAppServer(xiaohongshuService *XiaohongshuService) *AppServer {
|
||||||
return &AppServer{
|
appServer := &AppServer{
|
||||||
xiaohongshuService: xiaohongshuService,
|
xiaohongshuService: xiaohongshuService,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化 MCP Server(需要在创建 appServer 之后,因为工具注册需要访问 appServer)
|
||||||
|
appServer.mcpServer = InitMCPServer(appServer)
|
||||||
|
|
||||||
|
return appServer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start 启动服务器
|
// Start 启动服务器
|
||||||
@@ -51,16 +58,14 @@ func (s *AppServer) Start(port string) error {
|
|||||||
|
|
||||||
logrus.Infof("正在关闭服务器...")
|
logrus.Infof("正在关闭服务器...")
|
||||||
|
|
||||||
// 优雅关闭
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// 关闭 HTTP 服务器
|
|
||||||
if err := s.httpServer.Shutdown(ctx); err != nil {
|
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||||
logrus.Errorf("服务器关闭失败: %v", err)
|
logrus.Warnf("等待连接关闭超时,强制退出: %v", err)
|
||||||
return err
|
} else {
|
||||||
|
logrus.Infof("服务器已优雅关闭")
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infof("服务器已关闭")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
5
go.mod
5
go.mod
@@ -6,6 +6,8 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.10.1
|
||||||
github.com/go-rod/rod v0.116.2
|
github.com/go-rod/rod v0.116.2
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
|
github.com/mattn/go-runewidth v0.0.16
|
||||||
|
github.com/modelcontextprotocol/go-sdk v0.7.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
@@ -25,11 +27,11 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/go-rod/stealth v0.4.9 // indirect
|
github.com/go-rod/stealth v0.4.9 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/google/jsonschema-go v0.3.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
@@ -37,6 +39,7 @@ require (
|
|||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
|
||||||
github.com/ysmood/fetchup v0.2.3 // indirect
|
github.com/ysmood/fetchup v0.2.3 // indirect
|
||||||
github.com/ysmood/goob v0.4.0 // indirect
|
github.com/ysmood/goob v0.4.0 // indirect
|
||||||
github.com/ysmood/got v0.41.0 // indirect
|
github.com/ysmood/got v0.41.0 // indirect
|
||||||
|
|||||||
22
go.sum
22
go.sum
@@ -30,9 +30,11 @@ github.com/go-rod/stealth v0.4.9 h1:X2PmQk4DUF2wzw6GOsWjW/glb8K5ebnftbEvLh7MlZ4=
|
|||||||
github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc=
|
github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
|
||||||
|
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
|
||||||
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
|
||||||
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
@@ -47,6 +49,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/modelcontextprotocol/go-sdk v0.7.0 h1:XEQfn3bDx2cAdSUKty3tYEMll5dtRgBUDX88Q65fai0=
|
||||||
|
github.com/modelcontextprotocol/go-sdk v0.7.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -79,16 +83,12 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
github.com/xpzouying/headless_browser v0.0.2 h1:sLc4gqUT/5IyTruYIOfCW4aZLinq38hIdUHCHem1KYo=
|
|
||||||
github.com/xpzouying/headless_browser v0.0.2/go.mod h1:bQTSzGYHIipa1zwToMlOGHcXWDlvw8y33Cx5zzElekc=
|
|
||||||
github.com/xpzouying/headless_browser v0.1.0 h1:0FyMIzhe/If/VhEdDrs7T1fqm1gOZSCFrmMXI/1JM58=
|
|
||||||
github.com/xpzouying/headless_browser v0.1.0/go.mod h1:bQTSzGYHIipa1zwToMlOGHcXWDlvw8y33Cx5zzElekc=
|
|
||||||
github.com/xpzouying/headless_browser v0.2.0 h1:EmuHXDVzx0tAevHJUdETs8iT/eK+QqrLiybvGd1xZDA=
|
github.com/xpzouying/headless_browser v0.2.0 h1:EmuHXDVzx0tAevHJUdETs8iT/eK+QqrLiybvGd1xZDA=
|
||||||
github.com/xpzouying/headless_browser v0.2.0/go.mod h1:bQTSzGYHIipa1zwToMlOGHcXWDlvw8y33Cx5zzElekc=
|
github.com/xpzouying/headless_browser v0.2.0/go.mod h1:bQTSzGYHIipa1zwToMlOGHcXWDlvw8y33Cx5zzElekc=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
|
||||||
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
|
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
|
||||||
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
|
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
|
||||||
github.com/ysmood/fetchup v0.5.2 h1:P9w3OIA7RSNEEFvEmOiTq09IOu42C96PMyZ1MWd8TAs=
|
|
||||||
github.com/ysmood/fetchup v0.5.2/go.mod h1:yCv8s8itjsCul1LGXJ1Q+8EQnZcVjfbZ4+l1zDm4StE=
|
|
||||||
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
|
||||||
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18=
|
||||||
github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk=
|
||||||
@@ -114,14 +114,12 @@ golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -12,9 +12,11 @@ func main() {
|
|||||||
var (
|
var (
|
||||||
headless bool
|
headless bool
|
||||||
binPath string // 浏览器二进制文件路径
|
binPath string // 浏览器二进制文件路径
|
||||||
|
port string
|
||||||
)
|
)
|
||||||
flag.BoolVar(&headless, "headless", true, "是否无头模式")
|
flag.BoolVar(&headless, "headless", true, "是否无头模式")
|
||||||
flag.StringVar(&binPath, "bin", "", "浏览器二进制文件路径")
|
flag.StringVar(&binPath, "bin", "", "浏览器二进制文件路径")
|
||||||
|
flag.StringVar(&port, "port", ":18060", "端口")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if len(binPath) == 0 {
|
if len(binPath) == 0 {
|
||||||
@@ -29,7 +31,7 @@ func main() {
|
|||||||
|
|
||||||
// 创建并启动应用服务器
|
// 创建并启动应用服务器
|
||||||
appServer := NewAppServer(xiaohongshuService)
|
appServer := NewAppServer(xiaohongshuService)
|
||||||
if err := appServer.Start(":18060"); err != nil {
|
if err := appServer.Start(port); err != nil {
|
||||||
logrus.Fatalf("failed to run server: %v", err)
|
logrus.Fatalf("failed to run server: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
226
mcp_server.go
Normal file
226
mcp_server.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MCP 工具参数结构体定义
|
||||||
|
|
||||||
|
// PublishContentArgs 发布内容的参数
|
||||||
|
type PublishContentArgs struct {
|
||||||
|
Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"`
|
||||||
|
Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"`
|
||||||
|
Images []string `json:"images" jsonschema:"图片路径列表(至少需要1张图片)。支持两种方式:1. HTTP/HTTPS图片链接(自动下载);2. 本地图片绝对路径(推荐,如:/Users/user/image.jpg)"`
|
||||||
|
Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchFeedsArgs 搜索内容的参数
|
||||||
|
type SearchFeedsArgs struct {
|
||||||
|
Keyword string `json:"keyword" jsonschema:"搜索关键词"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FeedDetailArgs 获取Feed详情的参数
|
||||||
|
type FeedDetailArgs struct {
|
||||||
|
FeedID string `json:"feed_id" jsonschema:"小红书笔记ID,从Feed列表获取"`
|
||||||
|
XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserProfileArgs 获取用户主页的参数
|
||||||
|
type UserProfileArgs struct {
|
||||||
|
UserID string `json:"user_id" jsonschema:"小红书用户ID,从Feed列表获取"`
|
||||||
|
XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostCommentArgs 发表评论的参数
|
||||||
|
type PostCommentArgs struct {
|
||||||
|
FeedID string `json:"feed_id" jsonschema:"小红书笔记ID,从Feed列表获取"`
|
||||||
|
XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"`
|
||||||
|
Content string `json:"content" jsonschema:"评论内容"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitMCPServer 初始化 MCP Server
|
||||||
|
func InitMCPServer(appServer *AppServer) *mcp.Server {
|
||||||
|
// 创建 MCP Server
|
||||||
|
server := mcp.NewServer(
|
||||||
|
&mcp.Implementation{
|
||||||
|
Name: "xiaohongshu-mcp",
|
||||||
|
Version: "2.0.0",
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 注册所有工具
|
||||||
|
registerTools(server, appServer)
|
||||||
|
|
||||||
|
logrus.Info("MCP Server initialized with official SDK")
|
||||||
|
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerTools 注册所有 MCP 工具
|
||||||
|
func registerTools(server *mcp.Server, appServer *AppServer) {
|
||||||
|
// 工具 1: 检查登录状态
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "check_login_status",
|
||||||
|
Description: "检查小红书登录状态",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||||
|
result := appServer.handleCheckLoginStatus(ctx)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具 2: 获取登录二维码
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "get_login_qrcode",
|
||||||
|
Description: "获取登录二维码(返回 Base64 图片和超时时间)",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||||
|
result := appServer.handleGetLoginQrcode(ctx)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具 3: 发布内容
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "publish_content",
|
||||||
|
Description: "发布小红书图文内容",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, args PublishContentArgs) (*mcp.CallToolResult, any, error) {
|
||||||
|
// 转换参数格式到现有的 handler
|
||||||
|
argsMap := map[string]interface{}{
|
||||||
|
"title": args.Title,
|
||||||
|
"content": args.Content,
|
||||||
|
"images": convertStringsToInterfaces(args.Images),
|
||||||
|
"tags": convertStringsToInterfaces(args.Tags),
|
||||||
|
}
|
||||||
|
result := appServer.handlePublishContent(ctx, argsMap)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具 4: 获取Feed列表
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "list_feeds",
|
||||||
|
Description: "获取用户发布的内容列表",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
|
||||||
|
result := appServer.handleListFeeds(ctx)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具 5: 搜索内容
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "search_feeds",
|
||||||
|
Description: "搜索小红书内容(需要已登录)",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, args SearchFeedsArgs) (*mcp.CallToolResult, any, error) {
|
||||||
|
argsMap := map[string]interface{}{
|
||||||
|
"keyword": args.Keyword,
|
||||||
|
}
|
||||||
|
result := appServer.handleSearchFeeds(ctx, argsMap)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具 6: 获取Feed详情
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "get_feed_detail",
|
||||||
|
Description: "获取小红书笔记详情,返回笔记内容、图片、作者信息、互动数据(点赞/收藏/分享数)及评论列表",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, args FeedDetailArgs) (*mcp.CallToolResult, any, error) {
|
||||||
|
argsMap := map[string]interface{}{
|
||||||
|
"feed_id": args.FeedID,
|
||||||
|
"xsec_token": args.XsecToken,
|
||||||
|
}
|
||||||
|
result := appServer.handleGetFeedDetail(ctx, argsMap)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具 7: 获取用户主页
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "user_profile",
|
||||||
|
Description: "获取小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, args UserProfileArgs) (*mcp.CallToolResult, any, error) {
|
||||||
|
argsMap := map[string]interface{}{
|
||||||
|
"user_id": args.UserID,
|
||||||
|
"xsec_token": args.XsecToken,
|
||||||
|
}
|
||||||
|
result := appServer.handleUserProfile(ctx, argsMap)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具 8: 发表评论
|
||||||
|
mcp.AddTool(server,
|
||||||
|
&mcp.Tool{
|
||||||
|
Name: "post_comment_to_feed",
|
||||||
|
Description: "发表评论到小红书笔记",
|
||||||
|
},
|
||||||
|
func(ctx context.Context, req *mcp.CallToolRequest, args PostCommentArgs) (*mcp.CallToolResult, any, error) {
|
||||||
|
argsMap := map[string]interface{}{
|
||||||
|
"feed_id": args.FeedID,
|
||||||
|
"xsec_token": args.XsecToken,
|
||||||
|
"content": args.Content,
|
||||||
|
}
|
||||||
|
result := appServer.handlePostComment(ctx, argsMap)
|
||||||
|
return convertToMCPResult(result), nil, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
logrus.Infof("Registered %d MCP tools", 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToMCPResult 将自定义的 MCPToolResult 转换为官方 SDK 的格式
|
||||||
|
func convertToMCPResult(result *MCPToolResult) *mcp.CallToolResult {
|
||||||
|
var contents []mcp.Content
|
||||||
|
for _, c := range result.Content {
|
||||||
|
switch c.Type {
|
||||||
|
case "text":
|
||||||
|
contents = append(contents, &mcp.TextContent{Text: c.Text})
|
||||||
|
case "image":
|
||||||
|
// 解码 base64 字符串为 []byte
|
||||||
|
imageData, err := base64.StdEncoding.DecodeString(c.Data)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to decode base64 image data")
|
||||||
|
// 如果解码失败,添加错误文本
|
||||||
|
contents = append(contents, &mcp.TextContent{
|
||||||
|
Text: "图片数据解码失败: " + err.Error(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
contents = append(contents, &mcp.ImageContent{
|
||||||
|
Data: imageData,
|
||||||
|
MIMEType: c.MimeType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mcp.CallToolResult{
|
||||||
|
Content: contents,
|
||||||
|
IsError: result.IsError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertStringsToInterfaces 辅助函数:将 []string 转换为 []interface{}
|
||||||
|
func convertStringsToInterfaces(strs []string) []interface{} {
|
||||||
|
result := make([]interface{}, len(strs))
|
||||||
|
for i, s := range strs {
|
||||||
|
result[i] = s
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
14
routes.go
14
routes.go
@@ -1,7 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupRoutes 设置路由配置
|
// setupRoutes 设置路由配置
|
||||||
@@ -20,8 +23,15 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
|
|||||||
// 健康检查
|
// 健康检查
|
||||||
router.GET("/health", healthHandler)
|
router.GET("/health", healthHandler)
|
||||||
|
|
||||||
// MCP 端点 - 使用 Streamable HTTP 协议
|
// MCP 端点 - 使用官方 SDK 的 Streamable HTTP Handler
|
||||||
mcpHandler := appServer.StreamableHTTPHandler()
|
mcpHandler := mcp.NewStreamableHTTPHandler(
|
||||||
|
func(r *http.Request) *mcp.Server {
|
||||||
|
return appServer.mcpServer
|
||||||
|
},
|
||||||
|
&mcp.StreamableHTTPOptions{
|
||||||
|
JSONResponse: true, // 支持 JSON 响应
|
||||||
|
},
|
||||||
|
)
|
||||||
router.Any("/mcp", gin.WrapH(mcpHandler))
|
router.Any("/mcp", gin.WrapH(mcpHandler))
|
||||||
router.Any("/mcp/*path", gin.WrapH(mcpHandler))
|
router.Any("/mcp/*path", gin.WrapH(mcpHandler))
|
||||||
|
|
||||||
|
|||||||
@@ -1,402 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StreamableHTTPHandler 处理 Streamable HTTP 协议的 MCP 请求
|
|
||||||
func (s *AppServer) StreamableHTTPHandler() http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 设置 CORS 头
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id")
|
|
||||||
|
|
||||||
// 处理 OPTIONS 请求
|
|
||||||
if r.Method == "OPTIONS" {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据方法处理
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
// GET 请求用于建立 SSE 连接(可选功能)
|
|
||||||
s.handleSSEConnection(w, r)
|
|
||||||
case "POST":
|
|
||||||
// POST 请求处理 JSON-RPC
|
|
||||||
s.handleJSONRPCRequest(w, r)
|
|
||||||
default:
|
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSSEConnection 处理 SSE 连接(可选,用于服务器推送)
|
|
||||||
func (s *AppServer) handleSSEConnection(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 检查是否支持 SSE
|
|
||||||
if !strings.Contains(r.Header.Get("Accept"), "text/event-stream") {
|
|
||||||
http.Error(w, "SSE not requested", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置 SSE 响应头
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
|
|
||||||
// 发送初始化消息
|
|
||||||
fmt.Fprintf(w, "event: open\n")
|
|
||||||
fmt.Fprintf(w, "data: {\"type\":\"connection\",\"status\":\"connected\"}\n\n")
|
|
||||||
|
|
||||||
if f, ok := w.(http.Flusher); ok {
|
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保持连接打开(实际使用中可以在这里推送通知)
|
|
||||||
<-r.Context().Done()
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleJSONRPCRequest 处理 JSON-RPC 请求
|
|
||||||
func (s *AppServer) handleJSONRPCRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// 读取请求体
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
s.sendStreamableError(w, nil, -32700, "Parse error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
// 解析 JSON-RPC 请求
|
|
||||||
var request JSONRPCRequest
|
|
||||||
if err := json.Unmarshal(body, &request); err != nil {
|
|
||||||
s.sendStreamableError(w, nil, -32700, "Parse error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.WithField("method", request.Method).Info("Received Streamable HTTP request")
|
|
||||||
|
|
||||||
// 检查 Accept 头,判断客户端是否支持 SSE
|
|
||||||
acceptSSE := strings.Contains(r.Header.Get("Accept"), "text/event-stream")
|
|
||||||
|
|
||||||
// 处理请求
|
|
||||||
response := s.processJSONRPCRequest(&request, r.Context())
|
|
||||||
|
|
||||||
// 如果需要 SSE 且是支持流式的方法,使用 SSE 响应
|
|
||||||
if acceptSSE && s.isStreamableMethod(request.Method) {
|
|
||||||
s.sendSSEResponse(w, response)
|
|
||||||
} else {
|
|
||||||
// 否则使用普通 JSON 响应
|
|
||||||
s.sendJSONResponse(w, response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// processJSONRPCRequest 处理 JSON-RPC 请求并返回响应
|
|
||||||
func (s *AppServer) processJSONRPCRequest(request *JSONRPCRequest, ctx context.Context) *JSONRPCResponse {
|
|
||||||
switch request.Method {
|
|
||||||
case "initialize":
|
|
||||||
return s.processInitialize(request)
|
|
||||||
case "initialized":
|
|
||||||
// 客户端确认初始化完成
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Result: map[string]interface{}{},
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
case "ping":
|
|
||||||
// 处理 ping 请求
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Result: map[string]interface{}{},
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
case "tools/list":
|
|
||||||
return s.processToolsList(request)
|
|
||||||
case "tools/call":
|
|
||||||
return s.processToolCall(ctx, request)
|
|
||||||
default:
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Error: &JSONRPCError{
|
|
||||||
Code: -32601,
|
|
||||||
Message: "Method not found",
|
|
||||||
},
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// processInitialize 处理初始化请求
|
|
||||||
func (s *AppServer) processInitialize(request *JSONRPCRequest) *JSONRPCResponse {
|
|
||||||
result := map[string]interface{}{
|
|
||||||
"protocolVersion": "2025-03-26", // 使用新的协议版本
|
|
||||||
"capabilities": map[string]interface{}{
|
|
||||||
"tools": map[string]interface{}{},
|
|
||||||
},
|
|
||||||
"serverInfo": map[string]interface{}{
|
|
||||||
"name": "xiaohongshu-mcp",
|
|
||||||
"version": "2.0.0",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Result: result,
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// processToolsList 处理工具列表请求
|
|
||||||
func (s *AppServer) processToolsList(request *JSONRPCRequest) *JSONRPCResponse {
|
|
||||||
tools := []map[string]interface{}{
|
|
||||||
{
|
|
||||||
"name": "check_login_status",
|
|
||||||
"description": "检查小红书登录状态",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_login_qrcode",
|
|
||||||
"description": "获取登录二维码(返回 Base64 图片和超时时间)",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "publish_content",
|
|
||||||
"description": "发布小红书图文内容",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"title": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "内容标题(小红书限制:最多20个中文字或英文单词)",
|
|
||||||
},
|
|
||||||
"content": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可",
|
|
||||||
},
|
|
||||||
"images": map[string]interface{}{
|
|
||||||
"type": "array",
|
|
||||||
"description": "图片路径列表(至少需要1张图片)。支持两种方式:1. HTTP/HTTPS图片链接(自动下载);2. 本地图片绝对路径(推荐,如:/Users/user/image.jpg)",
|
|
||||||
"items": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
"minItems": 1,
|
|
||||||
},
|
|
||||||
"tags": map[string]interface{}{
|
|
||||||
"type": "array",
|
|
||||||
"description": "话题标签列表(可选),如 [\"美食\", \"旅行\", \"生活\"]",
|
|
||||||
"items": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"title", "content", "images"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "list_feeds",
|
|
||||||
"description": "获取用户发布的内容列表",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "search_feeds",
|
|
||||||
"description": "搜索小红书内容(需要已登录)",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"keyword": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "搜索关键词",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"keyword"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "get_feed_detail",
|
|
||||||
"description": "获取小红书笔记详情,返回笔记内容、图片、作者信息、互动数据(点赞/收藏/分享数)及评论列表",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"feed_id": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "小红书笔记ID,从Feed列表获取",
|
|
||||||
},
|
|
||||||
"xsec_token": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "访问令牌,从Feed列表的xsecToken字段获取",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"feed_id", "xsec_token"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user_profile",
|
|
||||||
"description": "获取小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"user_id": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "小红书用户ID,从Feed列表获取",
|
|
||||||
},
|
|
||||||
"xsec_token": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "访问令牌,从Feed列表的xsecToken字段获取",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"user_id", "xsec_token"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "post_comment_to_feed",
|
|
||||||
"description": "发表评论到小红书笔记",
|
|
||||||
"inputSchema": map[string]interface{}{
|
|
||||||
"type": "object",
|
|
||||||
"properties": map[string]interface{}{
|
|
||||||
"feed_id": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "小红书笔记ID,从Feed列表获取",
|
|
||||||
},
|
|
||||||
"xsec_token": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "访问令牌,从Feed列表的xsecToken字段获取",
|
|
||||||
},
|
|
||||||
"content": map[string]interface{}{
|
|
||||||
"type": "string",
|
|
||||||
"description": "评论内容",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": []string{"feed_id", "xsec_token", "content"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Result: map[string]interface{}{
|
|
||||||
"tools": tools,
|
|
||||||
},
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// processToolCall 处理工具调用
|
|
||||||
func (s *AppServer) processToolCall(ctx context.Context, request *JSONRPCRequest) *JSONRPCResponse {
|
|
||||||
// 解析参数
|
|
||||||
params, ok := request.Params.(map[string]interface{})
|
|
||||||
if !ok {
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Error: &JSONRPCError{
|
|
||||||
Code: -32602,
|
|
||||||
Message: "Invalid params",
|
|
||||||
},
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toolName, _ := params["name"].(string)
|
|
||||||
toolArgs, _ := params["arguments"].(map[string]interface{})
|
|
||||||
|
|
||||||
var result *MCPToolResult
|
|
||||||
|
|
||||||
switch toolName {
|
|
||||||
case "check_login_status":
|
|
||||||
result = s.handleCheckLoginStatus(ctx)
|
|
||||||
case "get_login_qrcode":
|
|
||||||
result = s.handleGetLoginQrcode(ctx)
|
|
||||||
case "publish_content":
|
|
||||||
result = s.handlePublishContent(ctx, toolArgs)
|
|
||||||
case "list_feeds":
|
|
||||||
result = s.handleListFeeds(ctx)
|
|
||||||
case "search_feeds":
|
|
||||||
result = s.handleSearchFeeds(ctx, toolArgs)
|
|
||||||
case "get_feed_detail":
|
|
||||||
result = s.handleGetFeedDetail(ctx, toolArgs)
|
|
||||||
case "user_profile":
|
|
||||||
result = s.handleUserProfile(ctx, toolArgs)
|
|
||||||
case "post_comment_to_feed":
|
|
||||||
result = s.handlePostComment(ctx, toolArgs)
|
|
||||||
default:
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Error: &JSONRPCError{
|
|
||||||
Code: -32602,
|
|
||||||
Message: fmt.Sprintf("Unknown tool: %s", toolName),
|
|
||||||
},
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Result: result,
|
|
||||||
ID: request.ID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isStreamableMethod 判断方法是否支持流式响应
|
|
||||||
func (s *AppServer) isStreamableMethod(_ string) bool {
|
|
||||||
// 目前我们的方法都不需要流式响应
|
|
||||||
// 未来可以在这里添加支持流式的方法
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendJSONResponse 发送普通 JSON 响应
|
|
||||||
func (s *AppServer) sendJSONResponse(w http.ResponseWriter, response *JSONRPCResponse) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to encode response")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendSSEResponse 发送 SSE 响应
|
|
||||||
func (s *AppServer) sendSSEResponse(w http.ResponseWriter, response *JSONRPCResponse) {
|
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
|
||||||
w.Header().Set("Cache-Control", "no-cache")
|
|
||||||
w.Header().Set("Connection", "keep-alive")
|
|
||||||
|
|
||||||
// 将响应转换为 JSON
|
|
||||||
data, err := json.Marshal(response)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to marshal SSE response")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送 SSE 格式的响应
|
|
||||||
fmt.Fprintf(w, "data: %s\n\n", string(data))
|
|
||||||
|
|
||||||
if f, ok := w.(http.Flusher); ok {
|
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendStreamableError 发送错误响应
|
|
||||||
func (s *AppServer) sendStreamableError(w http.ResponseWriter, id interface{}, code int, message string) {
|
|
||||||
response := &JSONRPCResponse{
|
|
||||||
JSONRPC: "2.0",
|
|
||||||
Error: &JSONRPCError{
|
|
||||||
Code: code,
|
|
||||||
Message: message,
|
|
||||||
},
|
|
||||||
ID: id,
|
|
||||||
}
|
|
||||||
s.sendJSONResponse(w, response)
|
|
||||||
}
|
|
||||||
37
types.go
37
types.go
@@ -16,46 +16,15 @@ type SuccessResponse struct {
|
|||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON-RPC 相关类型
|
// MCP 相关类型(用于内部转换)
|
||||||
|
|
||||||
// JSONRPCRequest JSON-RPC 请求
|
// MCPToolResult MCP 工具结果(内部使用)
|
||||||
type JSONRPCRequest struct {
|
|
||||||
JSONRPC string `json:"jsonrpc"`
|
|
||||||
Method string `json:"method"`
|
|
||||||
Params any `json:"params,omitempty"`
|
|
||||||
ID any `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONRPCResponse JSON-RPC 响应
|
|
||||||
type JSONRPCResponse struct {
|
|
||||||
JSONRPC string `json:"jsonrpc"`
|
|
||||||
Result any `json:"result,omitempty"`
|
|
||||||
Error *JSONRPCError `json:"error,omitempty"`
|
|
||||||
ID any `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONRPCError JSON-RPC 错误
|
|
||||||
type JSONRPCError struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data any `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MCP 相关类型
|
|
||||||
|
|
||||||
// MCPToolCall MCP 工具调用
|
|
||||||
type MCPToolCall struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Arguments map[string]interface{} `json:"arguments"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MCPToolResult MCP 工具结果
|
|
||||||
type MCPToolResult struct {
|
type MCPToolResult struct {
|
||||||
Content []MCPContent `json:"content"`
|
Content []MCPContent `json:"content"`
|
||||||
IsError bool `json:"isError,omitempty"`
|
IsError bool `json:"isError,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MCPContent MCP 内容
|
// MCPContent MCP 内容(内部使用)
|
||||||
type MCPContent struct {
|
type MCPContent struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
|
|||||||
Reference in New Issue
Block a user