Files
xiaohongshu-mcp/mcp_server.go
Banghao Chi 8c3665a3de feat: publish with video (#171)
* feat: publish with video

* fix: add more timeout (network bandwidth + large files) and remove pop-up

* fix: remove excessive remove pop-up function
2025-09-29 00:34:47 +08:00

253 lines
8.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"`
}
// PublishVideoArgs 发布视频的参数(仅支持本地单个视频文件)
type PublishVideoArgs struct {
Title string `json:"title" jsonschema:"内容标题小红书限制最多20个中文字或英文单词"`
Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容所有话题标签都用tags参数来生成和提供即可"`
Video string `json:"video" jsonschema:"本地视频绝对路径(仅支持单个视频文件,如:/Users/user/video.mp4"`
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
},
)
// 工具 9: 发布视频(仅本地文件)
mcp.AddTool(server,
&mcp.Tool{
Name: "publish_with_video",
Description: "发布小红书视频内容(仅支持本地单个视频文件)",
},
func(ctx context.Context, req *mcp.CallToolRequest, args PublishVideoArgs) (*mcp.CallToolResult, any, error) {
argsMap := map[string]interface{}{
"title": args.Title,
"content": args.Content,
"video": args.Video,
"tags": convertStringsToInterfaces(args.Tags),
}
result := appServer.handlePublishVideo(ctx, argsMap)
return convertToMCPResult(result), nil, nil
},
)
logrus.Infof("Registered %d MCP tools", 9)
}
// 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
}