|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/base64" |
| 6 | + |
| 7 | + "github.com/modelcontextprotocol/go-sdk/mcp" |
| 8 | + "github.com/sirupsen/logrus" |
| 9 | +) |
| 10 | + |
| 11 | +// MCP 工具参数结构体定义 |
| 12 | + |
| 13 | +// PublishContentArgs 发布内容的参数 |
| 14 | +type PublishContentArgs struct { |
| 15 | + Title string `json:"title" jsonschema:"内容标题(小红书限制:最多20个中文字或英文单词)"` |
| 16 | + Content string `json:"content" jsonschema:"正文内容,不包含以#开头的标签内容,所有话题标签都用tags参数来生成和提供即可"` |
| 17 | + Images []string `json:"images" jsonschema:"图片路径列表(至少需要1张图片)。支持两种方式:1. HTTP/HTTPS图片链接(自动下载);2. 本地图片绝对路径(推荐,如:/Users/user/image.jpg)"` |
| 18 | + Tags []string `json:"tags,omitempty" jsonschema:"话题标签列表(可选参数),如 [美食, 旅行, 生活]"` |
| 19 | +} |
| 20 | + |
| 21 | +// SearchFeedsArgs 搜索内容的参数 |
| 22 | +type SearchFeedsArgs struct { |
| 23 | + Keyword string `json:"keyword" jsonschema:"搜索关键词"` |
| 24 | +} |
| 25 | + |
| 26 | +// FeedDetailArgs 获取Feed详情的参数 |
| 27 | +type FeedDetailArgs struct { |
| 28 | + FeedID string `json:"feed_id" jsonschema:"小红书笔记ID,从Feed列表获取"` |
| 29 | + XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"` |
| 30 | +} |
| 31 | + |
| 32 | +// UserProfileArgs 获取用户主页的参数 |
| 33 | +type UserProfileArgs struct { |
| 34 | + UserID string `json:"user_id" jsonschema:"小红书用户ID,从Feed列表获取"` |
| 35 | + XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"` |
| 36 | +} |
| 37 | + |
| 38 | +// PostCommentArgs 发表评论的参数 |
| 39 | +type PostCommentArgs struct { |
| 40 | + FeedID string `json:"feed_id" jsonschema:"小红书笔记ID,从Feed列表获取"` |
| 41 | + XsecToken string `json:"xsec_token" jsonschema:"访问令牌,从Feed列表的xsecToken字段获取"` |
| 42 | + Content string `json:"content" jsonschema:"评论内容"` |
| 43 | +} |
| 44 | + |
| 45 | +// InitMCPServer 初始化 MCP Server |
| 46 | +func InitMCPServer(appServer *AppServer) *mcp.Server { |
| 47 | + // 创建 MCP Server |
| 48 | + server := mcp.NewServer( |
| 49 | + &mcp.Implementation{ |
| 50 | + Name: "xiaohongshu-mcp", |
| 51 | + Version: "2.0.0", |
| 52 | + }, |
| 53 | + nil, |
| 54 | + ) |
| 55 | + |
| 56 | + // 注册所有工具 |
| 57 | + registerTools(server, appServer) |
| 58 | + |
| 59 | + logrus.Info("MCP Server initialized with official SDK") |
| 60 | + |
| 61 | + return server |
| 62 | +} |
| 63 | + |
| 64 | +// registerTools 注册所有 MCP 工具 |
| 65 | +func registerTools(server *mcp.Server, appServer *AppServer) { |
| 66 | + // 工具 1: 检查登录状态 |
| 67 | + mcp.AddTool(server, |
| 68 | + &mcp.Tool{ |
| 69 | + Name: "check_login_status", |
| 70 | + Description: "检查小红书登录状态", |
| 71 | + }, |
| 72 | + func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { |
| 73 | + result := appServer.handleCheckLoginStatus(ctx) |
| 74 | + return convertToMCPResult(result), nil, nil |
| 75 | + }, |
| 76 | + ) |
| 77 | + |
| 78 | + // 工具 2: 获取登录二维码 |
| 79 | + mcp.AddTool(server, |
| 80 | + &mcp.Tool{ |
| 81 | + Name: "get_login_qrcode", |
| 82 | + Description: "获取登录二维码(返回 Base64 图片和超时时间)", |
| 83 | + }, |
| 84 | + func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { |
| 85 | + result := appServer.handleGetLoginQrcode(ctx) |
| 86 | + return convertToMCPResult(result), nil, nil |
| 87 | + }, |
| 88 | + ) |
| 89 | + |
| 90 | + // 工具 3: 发布内容 |
| 91 | + mcp.AddTool(server, |
| 92 | + &mcp.Tool{ |
| 93 | + Name: "publish_content", |
| 94 | + Description: "发布小红书图文内容", |
| 95 | + }, |
| 96 | + func(ctx context.Context, req *mcp.CallToolRequest, args PublishContentArgs) (*mcp.CallToolResult, any, error) { |
| 97 | + // 转换参数格式到现有的 handler |
| 98 | + argsMap := map[string]interface{}{ |
| 99 | + "title": args.Title, |
| 100 | + "content": args.Content, |
| 101 | + "images": convertStringsToInterfaces(args.Images), |
| 102 | + "tags": convertStringsToInterfaces(args.Tags), |
| 103 | + } |
| 104 | + result := appServer.handlePublishContent(ctx, argsMap) |
| 105 | + return convertToMCPResult(result), nil, nil |
| 106 | + }, |
| 107 | + ) |
| 108 | + |
| 109 | + // 工具 4: 获取Feed列表 |
| 110 | + mcp.AddTool(server, |
| 111 | + &mcp.Tool{ |
| 112 | + Name: "list_feeds", |
| 113 | + Description: "获取用户发布的内容列表", |
| 114 | + }, |
| 115 | + func(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { |
| 116 | + result := appServer.handleListFeeds(ctx) |
| 117 | + return convertToMCPResult(result), nil, nil |
| 118 | + }, |
| 119 | + ) |
| 120 | + |
| 121 | + // 工具 5: 搜索内容 |
| 122 | + mcp.AddTool(server, |
| 123 | + &mcp.Tool{ |
| 124 | + Name: "search_feeds", |
| 125 | + Description: "搜索小红书内容(需要已登录)", |
| 126 | + }, |
| 127 | + func(ctx context.Context, req *mcp.CallToolRequest, args SearchFeedsArgs) (*mcp.CallToolResult, any, error) { |
| 128 | + argsMap := map[string]interface{}{ |
| 129 | + "keyword": args.Keyword, |
| 130 | + } |
| 131 | + result := appServer.handleSearchFeeds(ctx, argsMap) |
| 132 | + return convertToMCPResult(result), nil, nil |
| 133 | + }, |
| 134 | + ) |
| 135 | + |
| 136 | + // 工具 6: 获取Feed详情 |
| 137 | + mcp.AddTool(server, |
| 138 | + &mcp.Tool{ |
| 139 | + Name: "get_feed_detail", |
| 140 | + Description: "获取小红书笔记详情,返回笔记内容、图片、作者信息、互动数据(点赞/收藏/分享数)及评论列表", |
| 141 | + }, |
| 142 | + func(ctx context.Context, req *mcp.CallToolRequest, args FeedDetailArgs) (*mcp.CallToolResult, any, error) { |
| 143 | + argsMap := map[string]interface{}{ |
| 144 | + "feed_id": args.FeedID, |
| 145 | + "xsec_token": args.XsecToken, |
| 146 | + } |
| 147 | + result := appServer.handleGetFeedDetail(ctx, argsMap) |
| 148 | + return convertToMCPResult(result), nil, nil |
| 149 | + }, |
| 150 | + ) |
| 151 | + |
| 152 | + // 工具 7: 获取用户主页 |
| 153 | + mcp.AddTool(server, |
| 154 | + &mcp.Tool{ |
| 155 | + Name: "user_profile", |
| 156 | + Description: "获取小红书用户主页,返回用户基本信息,关注、粉丝、获赞量及其笔记内容", |
| 157 | + }, |
| 158 | + func(ctx context.Context, req *mcp.CallToolRequest, args UserProfileArgs) (*mcp.CallToolResult, any, error) { |
| 159 | + argsMap := map[string]interface{}{ |
| 160 | + "user_id": args.UserID, |
| 161 | + "xsec_token": args.XsecToken, |
| 162 | + } |
| 163 | + result := appServer.handleUserProfile(ctx, argsMap) |
| 164 | + return convertToMCPResult(result), nil, nil |
| 165 | + }, |
| 166 | + ) |
| 167 | + |
| 168 | + // 工具 8: 发表评论 |
| 169 | + mcp.AddTool(server, |
| 170 | + &mcp.Tool{ |
| 171 | + Name: "post_comment_to_feed", |
| 172 | + Description: "发表评论到小红书笔记", |
| 173 | + }, |
| 174 | + func(ctx context.Context, req *mcp.CallToolRequest, args PostCommentArgs) (*mcp.CallToolResult, any, error) { |
| 175 | + argsMap := map[string]interface{}{ |
| 176 | + "feed_id": args.FeedID, |
| 177 | + "xsec_token": args.XsecToken, |
| 178 | + "content": args.Content, |
| 179 | + } |
| 180 | + result := appServer.handlePostComment(ctx, argsMap) |
| 181 | + return convertToMCPResult(result), nil, nil |
| 182 | + }, |
| 183 | + ) |
| 184 | + |
| 185 | + logrus.Infof("Registered %d MCP tools", 8) |
| 186 | +} |
| 187 | + |
| 188 | +// convertToMCPResult 将自定义的 MCPToolResult 转换为官方 SDK 的格式 |
| 189 | +func convertToMCPResult(result *MCPToolResult) *mcp.CallToolResult { |
| 190 | + var contents []mcp.Content |
| 191 | + for _, c := range result.Content { |
| 192 | + switch c.Type { |
| 193 | + case "text": |
| 194 | + contents = append(contents, &mcp.TextContent{Text: c.Text}) |
| 195 | + case "image": |
| 196 | + // 解码 base64 字符串为 []byte |
| 197 | + imageData, err := base64.StdEncoding.DecodeString(c.Data) |
| 198 | + if err != nil { |
| 199 | + logrus.WithError(err).Error("Failed to decode base64 image data") |
| 200 | + // 如果解码失败,添加错误文本 |
| 201 | + contents = append(contents, &mcp.TextContent{ |
| 202 | + Text: "图片数据解码失败: " + err.Error(), |
| 203 | + }) |
| 204 | + } else { |
| 205 | + contents = append(contents, &mcp.ImageContent{ |
| 206 | + Data: imageData, |
| 207 | + MIMEType: c.MimeType, |
| 208 | + }) |
| 209 | + } |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + return &mcp.CallToolResult{ |
| 214 | + Content: contents, |
| 215 | + IsError: result.IsError, |
| 216 | + } |
| 217 | +} |
| 218 | + |
| 219 | +// convertStringsToInterfaces 辅助函数:将 []string 转换为 []interface{} |
| 220 | +func convertStringsToInterfaces(strs []string) []interface{} { |
| 221 | + result := make([]interface{}, len(strs)) |
| 222 | + for i, s := range strs { |
| 223 | + result[i] = s |
| 224 | + } |
| 225 | + return result |
| 226 | +} |
0 commit comments