Files
xiaohongshu-mcp/mcp_server.go
zy 58faab15c4 mcp: add image url support (#1)
* refactor: remove redundant validation in service and clean up code

- Remove validatePublishRequest function as gin binding already handles validation
- Remove unused errors import
- Simplify PublishContent method by relying on gin's built-in validation
- Add comprehensive image processing support with URL download capability

* 添加 MCP 说明文档,完善教程
2025-08-10 15:44:15 +08:00

284 lines
7.0 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/json"
"fmt"
"net/http"
"github.com/sirupsen/logrus"
)
// JSON-RPC 结构定义
type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
ID interface{} `json:"id"`
}
type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result interface{} `json:"result,omitempty"`
Error *JSONRPCError `json:"error,omitempty"`
ID interface{} `json:"id"`
}
type JSONRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
type MCPToolCall struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
}
type MCPToolResult struct {
Content []MCPContent `json:"content"`
IsError bool `json:"isError,omitempty"`
}
type MCPContent struct {
Type string `json:"type"`
Text string `json:"text"`
}
// MCP 工具处理函数
// handleCheckLoginStatus 处理检查登录状态
func handleCheckLoginStatus(ctx context.Context) *MCPToolResult {
logrus.Info("MCP: 检查登录状态")
status, err := 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 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 := 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,
}},
}
}
// handleMCPRequest 处理 MCP 请求
func handleMCPRequest(w http.ResponseWriter, r *http.Request) {
var req JSONRPCRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logrus.Errorf("解析请求失败: %v", err)
sendJSONRPCError(w, req.ID, -32700, "Parse error", nil)
return
}
logrus.Infof("收到MCP请求: %s", req.Method)
switch req.Method {
case "initialize":
handleInitialize(w, req)
case "tools/list":
handleToolsList(w, req)
case "tools/call":
handleToolsCall(w, r, req)
default:
logrus.Warnf("不支持的方法: %s", req.Method)
sendJSONRPCError(w, req.ID, -32601, "Method not found", nil)
}
}
// handleInitialize 处理初始化请求
func 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",
},
}
sendJSONRPCResponse(w, req.ID, result)
}
// handleToolsList 处理工具列表请求
func 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"},
},
},
}
result := map[string]interface{}{
"tools": tools,
}
sendJSONRPCResponse(w, req.ID, result)
}
// handleToolsCall 处理工具调用请求
func 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)
sendJSONRPCError(w, req.ID, -32602, "Invalid params", nil)
return
}
ctx := r.Context()
var result *MCPToolResult
switch toolCall.Name {
case "check_login_status":
result = handleCheckLoginStatus(ctx)
case "publish_content":
result = handlePublishContent(ctx, toolCall.Arguments)
default:
logrus.Warnf("不支持的工具: %s", toolCall.Name)
sendJSONRPCError(w, req.ID, -32601, "Tool not found", nil)
return
}
sendJSONRPCResponse(w, req.ID, result)
}
// sendJSONRPCResponse 发送JSON-RPC响应
func sendJSONRPCResponse(w http.ResponseWriter, id interface{}, result interface{}) {
response := JSONRPCResponse{
JSONRPC: "2.0",
Result: result,
ID: id,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// sendJSONRPCError 发送JSON-RPC错误响应
func 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状态码
json.NewEncoder(w).Encode(response)
}
// createMCPHandler 创建MCP HTTP处理器
func 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 请求
handleMCPRequest(w, r)
}
}