From 9b086c162ec4d8a358c91ad8c0b2e0410a9d3254 Mon Sep 17 00:00:00 2001 From: zy Date: Sat, 6 Sep 2025 19:26:05 +0800 Subject: [PATCH] fix: implement MCP Streamable HTTP protocol for Cursor integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace basic JSON-RPC implementation with Streamable HTTP protocol - Add support for GET (SSE) and POST (JSON-RPC) requests - Update protocol version from 2024-11-05 to 2025-03-26 - Fix list_feeds tool definition to match actual API (remove unused page/pageSize params) - Add ping method support for MCP Inspector - Update Cursor configuration to use url field instead of curl command - Add comprehensive MCP integration documentation Fixes #32 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .cursor/mcp.json | 8 + MCP_README.md | 62 ++++++-- handlers_mcp.go | 370 --------------------------------------------- mcp_handlers.go | 165 ++++++++++++++++++++ routes.go | 4 +- streamable_http.go | 324 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 547 insertions(+), 386 deletions(-) create mode 100644 .cursor/mcp.json delete mode 100644 handlers_mcp.go create mode 100644 mcp_handlers.go create mode 100644 streamable_http.go diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000..fa2aa23 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "xiaohongshu-mcp": { + "url": "http://localhost:18060/mcp", + "description": "小红书内容发布服务 - MCP Streamable HTTP" + } + } +} diff --git a/MCP_README.md b/MCP_README.md index 58bd0ea..57710cd 100644 --- a/MCP_README.md +++ b/MCP_README.md @@ -65,6 +65,8 @@ claude mcp add --transport http xiaohongshu-mcp http://localhost:18060/mcp ### Cursor +**重要提示**:Cursor 支持三种 MCP 传输方式:stdio、SSE 和 Streamable HTTP。对于 HTTP 服务器,应该使用 `url` 字段而不是 `command` 和 `args`。 + #### 配置文件的方式 创建或编辑 MCP 配置文件: @@ -76,17 +78,8 @@ claude mcp add --transport http xiaohongshu-mcp http://localhost:18060/mcp { "mcpServers": { "xiaohongshu-mcp": { - "command": "curl", - "args": [ - "-X", - "POST", - "http://localhost:18060/mcp", - "-H", - "Content-Type: application/json", - "-d", - "@-" - ], - "description": "小红书内容发布服务" + "url": "http://localhost:18060/mcp", + "description": "小红书内容发布服务 - MCP Streamable HTTP" } } } @@ -95,6 +88,13 @@ claude mcp add --transport http xiaohongshu-mcp http://localhost:18060/mcp **全局配置**: 在用户目录创建 `~/.cursor/mcp.json` (同样内容) +#### 使用步骤 + +1. 确保小红书 MCP 服务正在运行 +2. 保存配置文件后,重启 Cursor +3. 在 Cursor 聊天中,工具应该自动可用 +4. 可以通过聊天界面的 "Available Tools" 查看已连接的 MCP 工具 + **Demo** ![cursor_mcp_demo](./assets/cursor_mcp_01.png) @@ -162,9 +162,10 @@ npx @modelcontextprotocol/inspector 连接成功后,可使用以下 MCP 工具: -- `check_login_status` - 检查小红书登录状态 -- `publish_content` - 发布图文内容到小红书 -- `list_feeds` - 获取小红书首页推荐列表 +- `check_login_status` - 检查小红书登录状态(无参数) +- `publish_content` - 发布图文内容到小红书(需要:title, content, 可选:images, video) +- `list_feeds` - 获取小红书首页推荐列表(无参数) +- `search_feeds` - 搜索小红书内容(需要:keyword) ## 📝 使用示例 @@ -190,6 +191,26 @@ npx @modelcontextprotocol/inspector } ``` +### 获取推荐列表 + +```json +{ + "name": "list_feeds", + "arguments": {} +} +``` + +### 搜索内容 + +```json +{ + "name": "search_feeds", + "arguments": { + "keyword": "搜索关键词" + } +} +``` + ## ⚠️ 注意事项 1. **首次使用需要登录**:运行 `go run cmd/login/main.go` 完成登录 @@ -204,8 +225,21 @@ npx @modelcontextprotocol/inspector - 确认端口未被占用 - 检查防火墙设置 +### Cursor 连接问题 + +- 确保使用正确的配置格式:HTTP 服务器使用 `url` 字段,而不是 `command` + `args` +- 重启 Cursor 应用以加载新的 MCP 配置 +- 检查是否有 "Available Tools" 显示在聊天界面中 + +### MCP Inspector 测试 + +- 使用 MCP Inspector 测试连接:`npx @modelcontextprotocol/inspector` +- 测试 Ping Server 功能验证连接 +- 检查 List Tools 是否返回 4 个工具 + ### 工具调用失败 - 确认已完成小红书登录 - 检查图片 URL 或路径是否有效 - 查看服务日志获取详细错误信息 +- 确保工具参数格式正确(特别注意 `list_feeds` 不需要参数) diff --git a/handlers_mcp.go b/handlers_mcp.go deleted file mode 100644 index cab9589..0000000 --- a/handlers_mcp.go +++ /dev/null @@ -1,370 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - - "github.com/sirupsen/logrus" -) - -// MCP 工具处理函数 - -// handleCheckLoginStatus 处理检查登录状态 -func (s *AppServer) handleCheckLoginStatus(ctx context.Context) *MCPToolResult { - logrus.Info("MCP: 检查登录状态") - - status, err := s.xiaohongshuService.CheckLoginStatus(ctx) - if err != nil { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: "检查登录状态失败: " + err.Error(), - }}, - IsError: true, - } - } - - resultText := fmt.Sprintf("登录状态检查成功: %+v", status) - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: resultText, - }}, - } -} - -// handlePublishContent 处理发布内容 -func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]interface{}) *MCPToolResult { - logrus.Info("MCP: 发布内容") - - // 解析参数 - title, _ := args["title"].(string) - content, _ := args["content"].(string) - imagePathsInterface, _ := args["images"].([]interface{}) - - var imagePaths []string - for _, path := range imagePathsInterface { - if pathStr, ok := path.(string); ok { - imagePaths = append(imagePaths, pathStr) - } - } - - logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d", title, len(imagePaths)) - - // 构建发布请求 - req := &PublishRequest{ - Title: title, - Content: content, - Images: imagePaths, - } - - // 执行发布 - result, err := s.xiaohongshuService.PublishContent(ctx, req) - if err != nil { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: "发布失败: " + err.Error(), - }}, - IsError: true, - } - } - - resultText := fmt.Sprintf("内容发布成功: %+v", result) - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: resultText, - }}, - } -} - -// handleListFeeds 处理获取Feeds列表 -func (s *AppServer) handleListFeeds(ctx context.Context) *MCPToolResult { - logrus.Info("MCP: 获取Feeds列表") - - result, err := s.xiaohongshuService.ListFeeds(ctx) - if err != nil { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: "获取Feeds列表失败: " + err.Error(), - }}, - IsError: true, - } - } - - // 格式化输出,转换为JSON字符串 - jsonData, err := json.MarshalIndent(result, "", " ") - if err != nil { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: fmt.Sprintf("获取Feeds列表成功,但序列化失败: %v", err), - }}, - IsError: true, - } - } - - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: string(jsonData), - }}, - } -} - -// handleSearchFeeds 处理搜索Feeds -func (s *AppServer) handleSearchFeeds(ctx context.Context, args map[string]interface{}) *MCPToolResult { - logrus.Info("MCP: 搜索Feeds") - - // 解析参数 - keyword, ok := args["keyword"].(string) - if !ok || keyword == "" { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: "搜索Feeds失败: 缺少关键词参数", - }}, - IsError: true, - } - } - - logrus.Infof("MCP: 搜索Feeds - 关键词: %s", keyword) - - result, err := s.xiaohongshuService.SearchFeeds(ctx, keyword) - if err != nil { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: "搜索Feeds失败: " + err.Error(), - }}, - IsError: true, - } - } - - // 格式化输出,转换为JSON字符串 - jsonData, err := json.MarshalIndent(result, "", " ") - if err != nil { - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: fmt.Sprintf("搜索Feeds成功,但序列化失败: %v", err), - }}, - IsError: true, - } - } - - return &MCPToolResult{ - Content: []MCPContent{{ - Type: "text", - Text: string(jsonData), - }}, - } -} - -// handleMCPRequest 处理 MCP 请求 -func (s *AppServer) handleMCPRequest(w http.ResponseWriter, r *http.Request) { - var req JSONRPCRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - logrus.Errorf("解析请求失败: %v", err) - s.sendJSONRPCError(w, req.ID, -32700, "Parse error", nil) - return - } - - logrus.Infof("收到MCP请求: %s", req.Method) - - switch req.Method { - case "initialize": - s.handleInitialize(w, req) - case "tools/list": - s.handleToolsList(w, req) - case "tools/call": - s.handleToolsCall(w, r, req) - case "notifications/initialized": - // 客户端通知已初始化完成,不需要响应 - logrus.Info("MCP: 客户端已初始化完成") - // 通知不需要响应 - return - case "notifications/cancelled": - // 客户端通知取消请求,记录日志即可 - logrus.Info("MCP: 收到取消请求通知") - // 通知不需要响应 - return - default: - logrus.Warnf("不支持的方法: %s", req.Method) - s.sendJSONRPCError(w, req.ID, -32601, "Method not found", nil) - } -} - -// handleInitialize 处理初始化请求 -func (s *AppServer) handleInitialize(w http.ResponseWriter, req JSONRPCRequest) { - result := map[string]interface{}{ - "protocolVersion": "2024-11-05", - "capabilities": map[string]interface{}{ - "tools": map[string]interface{}{}, - }, - "serverInfo": map[string]interface{}{ - "name": "xiaohongshu-mcp", - "version": "v1.0.0", - }, - } - - s.sendJSONRPCResponse(w, req.ID, result) -} - -// handleToolsList 处理工具列表请求 -func (s *AppServer) handleToolsList(w http.ResponseWriter, req JSONRPCRequest) { - tools := []map[string]interface{}{ - { - "name": "check_login_status", - "description": "检查小红书登录状态", - "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": "发布内容的标题", - }, - "content": map[string]interface{}{ - "type": "string", - "description": "发布内容的正文", - }, - "images": map[string]interface{}{ - "type": "array", - "items": map[string]string{"type": "string"}, - "description": "图片路径或URL列表(支持本地文件路径和HTTP/HTTPS图片URL,至少一个)", - "minItems": 1, - }, - }, - "required": []string{"title", "content", "images"}, - }, - }, - { - "name": "list_feeds", - "description": "获取小红书首页Feeds列表", - "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"}, - }, - }, - } - - result := map[string]interface{}{ - "tools": tools, - } - - s.sendJSONRPCResponse(w, req.ID, result) -} - -// handleToolsCall 处理工具调用请求 -func (s *AppServer) handleToolsCall(w http.ResponseWriter, r *http.Request, req JSONRPCRequest) { - var toolCall MCPToolCall - paramsBytes, _ := json.Marshal(req.Params) - if err := json.Unmarshal(paramsBytes, &toolCall); err != nil { - logrus.Errorf("解析工具调用参数失败: %v", err) - s.sendJSONRPCError(w, req.ID, -32602, "Invalid params", nil) - return - } - - ctx := r.Context() - var result *MCPToolResult - - switch toolCall.Name { - case "check_login_status": - result = s.handleCheckLoginStatus(ctx) - case "publish_content": - result = s.handlePublishContent(ctx, toolCall.Arguments) - case "list_feeds": - result = s.handleListFeeds(ctx) - case "search_feeds": - result = s.handleSearchFeeds(ctx, toolCall.Arguments) - default: - logrus.Warnf("不支持的工具: %s", toolCall.Name) - s.sendJSONRPCError(w, req.ID, -32601, "Tool not found", nil) - return - } - - s.sendJSONRPCResponse(w, req.ID, result) -} - -// sendJSONRPCResponse 发送JSON-RPC响应 -func (s *AppServer) sendJSONRPCResponse(w http.ResponseWriter, id interface{}, result interface{}) { - response := JSONRPCResponse{ - JSONRPC: "2.0", - Result: result, - ID: id, - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(response); err != nil { - logrus.Errorf("Failed to encode JSON-RPC response: %v", err) - } -} - -// sendJSONRPCError 发送JSON-RPC错误响应 -func (s *AppServer) sendJSONRPCError(w http.ResponseWriter, id interface{}, code int, message string, data interface{}) { - response := JSONRPCResponse{ - JSONRPC: "2.0", - Error: &JSONRPCError{ - Code: code, - Message: message, - Data: data, - }, - ID: id, - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) // JSON-RPC错误仍然返回200状态码 - if err := json.NewEncoder(w).Encode(response); err != nil { - logrus.Errorf("Failed to encode JSON-RPC error response: %v", err) - } -} - -// createMCPHandler 创建MCP HTTP处理器 -func (s *AppServer) createMCPHandler() 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", "POST, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") - w.Header().Set("Content-Type", "application/json") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - if r.Method != "POST" { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // 处理 MCP JSON-RPC 请求 - s.handleMCPRequest(w, r) - } -} diff --git a/mcp_handlers.go b/mcp_handlers.go new file mode 100644 index 0000000..5a857a6 --- /dev/null +++ b/mcp_handlers.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/sirupsen/logrus" +) + +// MCP 工具处理函数 + +// handleCheckLoginStatus 处理检查登录状态 +func (s *AppServer) handleCheckLoginStatus(ctx context.Context) *MCPToolResult { + logrus.Info("MCP: 检查登录状态") + + status, err := s.xiaohongshuService.CheckLoginStatus(ctx) + if err != nil { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: "检查登录状态失败: " + err.Error(), + }}, + IsError: true, + } + } + + resultText := fmt.Sprintf("登录状态检查成功: %+v", status) + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: resultText, + }}, + } +} + +// handlePublishContent 处理发布内容 +func (s *AppServer) handlePublishContent(ctx context.Context, args map[string]interface{}) *MCPToolResult { + logrus.Info("MCP: 发布内容") + + // 解析参数 + title, _ := args["title"].(string) + content, _ := args["content"].(string) + imagePathsInterface, _ := args["images"].([]interface{}) + + var imagePaths []string + for _, path := range imagePathsInterface { + if pathStr, ok := path.(string); ok { + imagePaths = append(imagePaths, pathStr) + } + } + + logrus.Infof("MCP: 发布内容 - 标题: %s, 图片数量: %d", title, len(imagePaths)) + + // 构建发布请求 + req := &PublishRequest{ + Title: title, + Content: content, + Images: imagePaths, + } + + // 执行发布 + result, err := s.xiaohongshuService.PublishContent(ctx, req) + if err != nil { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: "发布失败: " + err.Error(), + }}, + IsError: true, + } + } + + resultText := fmt.Sprintf("内容发布成功: %+v", result) + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: resultText, + }}, + } +} + +// handleListFeeds 处理获取Feeds列表 +func (s *AppServer) handleListFeeds(ctx context.Context) *MCPToolResult { + logrus.Info("MCP: 获取Feeds列表") + + result, err := s.xiaohongshuService.ListFeeds(ctx) + if err != nil { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: "获取Feeds列表失败: " + err.Error(), + }}, + IsError: true, + } + } + + // 格式化输出,转换为JSON字符串 + jsonData, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: fmt.Sprintf("获取Feeds列表成功,但序列化失败: %v", err), + }}, + IsError: true, + } + } + + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: string(jsonData), + }}, + } +} + +// handleSearchFeeds 处理搜索Feeds +func (s *AppServer) handleSearchFeeds(ctx context.Context, args map[string]interface{}) *MCPToolResult { + logrus.Info("MCP: 搜索Feeds") + + // 解析参数 + keyword, ok := args["keyword"].(string) + if !ok || keyword == "" { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: "搜索Feeds失败: 缺少关键词参数", + }}, + IsError: true, + } + } + + logrus.Infof("MCP: 搜索Feeds - 关键词: %s", keyword) + + result, err := s.xiaohongshuService.SearchFeeds(ctx, keyword) + if err != nil { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: "搜索Feeds失败: " + err.Error(), + }}, + IsError: true, + } + } + + // 格式化输出,转换为JSON字符串 + jsonData, err := json.MarshalIndent(result, "", " ") + if err != nil { + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: fmt.Sprintf("搜索Feeds成功,但序列化失败: %v", err), + }}, + IsError: true, + } + } + + return &MCPToolResult{ + Content: []MCPContent{{ + Type: "text", + Text: string(jsonData), + }}, + } +} \ No newline at end of file diff --git a/routes.go b/routes.go index 3031c3f..0cb8670 100644 --- a/routes.go +++ b/routes.go @@ -20,8 +20,8 @@ func setupRoutes(appServer *AppServer) *gin.Engine { // 健康检查 router.GET("/health", healthHandler) - // MCP 端点 - 使用 SSE 协议 - mcpHandler := appServer.createMCPHandler() + // MCP 端点 - 使用 Streamable HTTP 协议 + mcpHandler := appServer.StreamableHTTPHandler() router.Any("/mcp", gin.WrapH(mcpHandler)) router.Any("/mcp/*path", gin.WrapH(mcpHandler)) diff --git a/streamable_http.go b/streamable_http.go new file mode 100644 index 0000000..6fbd6d5 --- /dev/null +++ b/streamable_http.go @@ -0,0 +1,324 @@ +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": "publish_content", + "description": "发布小红书内容(支持图文或视频)", + "inputSchema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "title": map[string]interface{}{ + "type": "string", + "description": "内容标题", + }, + "content": map[string]interface{}{ + "type": "string", + "description": "正文内容", + }, + "images": map[string]interface{}{ + "type": "array", + "description": "图片路径列表(发布图文时使用)", + "items": map[string]interface{}{ + "type": "string", + }, + }, + "video": map[string]interface{}{ + "type": "string", + "description": "视频文件路径(发布视频时使用)", + }, + }, + "required": []string{"title", "content"}, + }, + }, + { + "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"}, + }, + }, + } + + 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 "publish_content": + result = s.handlePublishContent(ctx, toolArgs) + case "list_feeds": + result = s.handleListFeeds(ctx) + case "search_feeds": + result = s.handleSearchFeeds(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) +}