Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
X
xhs-publish-mcp
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
郭阔
xhs-publish-mcp
Commits
86ed10c7
Commit
86ed10c7
authored
Nov 11, 2026
by
郭阔
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add a publish-only MCP mode for shared deployments
parent
3fe25414
Changes
6
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
161 additions
and
70 deletions
+161
-70
README.md
README.md
+24
-0
app_server.go
app_server.go
+6
-0
main.go
main.go
+10
-4
mcp_server.go
mcp_server.go
+66
-61
routes.go
routes.go
+9
-5
routes_test.go
routes_test.go
+46
-0
No files found.
README.md
View file @
86ed10c7
...
@@ -10,6 +10,30 @@
...
@@ -10,6 +10,30 @@
MCP for 小红书 / xiaohongshu.com。让你的 AI 助手直接访问小红书数据。
MCP for 小红书 / xiaohongshu.com。让你的 AI 助手直接访问小红书数据。
## 内网图文发布模式
`-publish-only`
只注册图文发布、主站登录状态、当前账号主页和搜索四个 MCP 工具。主页用于确认已发布笔记及链接,搜索兼容旧版 Skill。该模式要求配置
`AUTH_TOKEN`
,客户端使用
`Authorization: Bearer <令牌>`
。默认启动模式保留完整工具集。
支持 Windows x64 和 Linux x64。安装 Go 1.27.3 或兼容版本后,从本仓库构建:
```
sh
# Linux x64
go build
-o
xhs-mcp
.
go build
-o
xhs-login ./cmd/login
```
```
powershell
# Windows x64
go build -o xhs-mcp.exe
.
go build -o xhs-login.exe ./cmd/login
```
在同一工作目录运行登录工具和服务。管理员可在带桌面环境的部署机运行登录工具并扫码。无桌面的 Linux 服务器可先在回环地址临时启动完整模式,通过 SSH 隧道调用
`get_login_qrcode`
,扫码成功后关闭临时服务,再启动图文发布模式。登录文件路径可用
`COOKIES_PATH`
固定。Linux 所需 Chromium 系统库见
[
Dockerfile
](
./Dockerfile
)
。
设置
`AUTH_TOKEN`
后运行
`./xhs-mcp -publish-only -port 127.0.0.1:18060`
,Windows 对应
`./xhs-mcp.exe -publish-only -port 127.0.0.1:18060`
。通过
`/health`
验证服务已启动,再用
`check_login_status`
核对账号。
现有 Skill 仍连接编辑电脑的本机地址。供多人集中调用前,需要为 Skill 增加远程地址和令牌配置,并通过受控素材上传或受信任的素材域传递图片。服务接口支持读取部署机文件和下载图片,配置远程接入时应在入口限制素材来源。
### 🚀 快速开始:选择最适合你的版本
### 🚀 快速开始:选择最适合你的版本
> [!IMPORTANT]
> [!IMPORTANT]
...
...
app_server.go
View file @
86ed10c7
...
@@ -20,13 +20,19 @@ type AppServer struct {
...
@@ -20,13 +20,19 @@ type AppServer struct {
router
*
gin
.
Engine
router
*
gin
.
Engine
httpServer
*
http
.
Server
httpServer
*
http
.
Server
authToken
string
authToken
string
publishOnly
bool
}
}
// NewAppServer 创建新的应用服务器实例
// NewAppServer 创建新的应用服务器实例
func
NewAppServer
(
xiaohongshuService
*
XiaohongshuService
,
authToken
string
)
*
AppServer
{
func
NewAppServer
(
xiaohongshuService
*
XiaohongshuService
,
authToken
string
)
*
AppServer
{
return
newAppServer
(
xiaohongshuService
,
authToken
,
false
)
}
func
newAppServer
(
xiaohongshuService
*
XiaohongshuService
,
authToken
string
,
publishOnly
bool
)
*
AppServer
{
appServer
:=
&
AppServer
{
appServer
:=
&
AppServer
{
xiaohongshuService
:
xiaohongshuService
,
xiaohongshuService
:
xiaohongshuService
,
authToken
:
authToken
,
authToken
:
authToken
,
publishOnly
:
publishOnly
,
}
}
// 初始化 MCP Server(需要在创建 appServer 之后,因为工具注册需要访问 appServer)
// 初始化 MCP Server(需要在创建 appServer 之后,因为工具注册需要访问 appServer)
...
...
main.go
View file @
86ed10c7
...
@@ -15,6 +15,7 @@ var version = "dev"
...
@@ -15,6 +15,7 @@ var version = "dev"
func
main
()
{
func
main
()
{
var
(
var
(
publishOnly
bool
headless
bool
headless
bool
port
string
port
string
token
string
token
string
...
@@ -22,11 +23,16 @@ func main() {
...
@@ -22,11 +23,16 @@ func main() {
flag
.
BoolVar
(
&
headless
,
"headless"
,
true
,
"是否无头模式"
)
flag
.
BoolVar
(
&
headless
,
"headless"
,
true
,
"是否无头模式"
)
flag
.
StringVar
(
&
port
,
"port"
,
":18060"
,
"端口"
)
flag
.
StringVar
(
&
port
,
"port"
,
":18060"
,
"端口"
)
flag
.
StringVar
(
&
token
,
"token"
,
""
,
"鉴权 Token,留空则读取 AUTH_TOKEN"
)
flag
.
StringVar
(
&
token
,
"token"
,
""
,
"鉴权 Token,留空则读取 AUTH_TOKEN"
)
flag
.
BoolVar
(
&
publishOnly
,
"publish-only"
,
false
,
"仅提供图文发布、登录状态和笔记查询"
)
flag
.
Parse
()
flag
.
Parse
()
if
token
==
""
{
if
token
==
""
{
token
=
os
.
Getenv
(
"AUTH_TOKEN"
)
token
=
os
.
Getenv
(
"AUTH_TOKEN"
)
}
}
if
publishOnly
&&
token
==
""
{
logrus
.
Fatal
(
"图文发布模式需要配置 AUTH_TOKEN"
)
}
logrus
.
Infof
(
"xiaohongshu-mcp version: %s"
,
version
)
logrus
.
Infof
(
"xiaohongshu-mcp version: %s"
,
version
)
// 只用内置浏览器。启动时就备好,缺它直接退出,不拖到第一个请求才失败。
// 只用内置浏览器。启动时就备好,缺它直接退出,不拖到第一个请求才失败。
...
@@ -47,7 +53,7 @@ func main() {
...
@@ -47,7 +53,7 @@ func main() {
xiaohongshuService
:=
NewXiaohongshuService
()
xiaohongshuService
:=
NewXiaohongshuService
()
// 创建并启动应用服务器
// 创建并启动应用服务器
appServer
:=
NewAppServer
(
xiaohongshuService
,
token
)
appServer
:=
newAppServer
(
xiaohongshuService
,
token
,
publishOnly
)
if
err
:=
appServer
.
Start
(
port
);
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
)
}
}
...
...
mcp_server.go
View file @
86ed10c7
...
@@ -195,39 +195,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
...
@@ -195,39 +195,7 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
}),
}),
)
)
// 工具 2: 获取登录二维码
// 发布内容
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
Name
:
"get_login_qrcode"
,
Description
:
"获取登录二维码(返回 Base64 图片和超时时间)"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"Get Login QR Code"
,
ReadOnlyHint
:
true
,
},
},
withPanicRecovery
(
"get_login_qrcode"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
_
any
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handleGetLoginQrcode
(
ctx
)
return
convertToMCPResult
(
result
),
nil
,
nil
}),
)
// 工具 3: 删除 cookies(登录重置)
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
Name
:
"delete_cookies"
,
Description
:
"删除 cookies 文件,重置登录状态。删除后需要重新登录。"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"Delete Cookies"
,
DestructiveHint
:
boolPtr
(
true
),
},
},
withPanicRecovery
(
"delete_cookies"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
_
any
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handleDeleteCookies
(
ctx
)
return
convertToMCPResult
(
result
),
nil
,
nil
}),
)
// 工具 4: 发布内容
publishContentSchema
,
err
:=
jsonschema
.
For
[
PublishContentArgs
](
nil
)
publishContentSchema
,
err
:=
jsonschema
.
For
[
PublishContentArgs
](
nil
)
if
err
!=
nil
{
if
err
!=
nil
{
panic
(
fmt
.
Sprintf
(
"publish_content 参数 schema 生成失败: %v"
,
err
))
panic
(
fmt
.
Sprintf
(
"publish_content 参数 schema 生成失败: %v"
,
err
))
...
@@ -263,34 +231,87 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
...
@@ -263,34 +231,87 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
}),
}),
)
)
//
工具 5: 获取Feed列表
//
搜索内容
mcp
.
AddTool
(
server
,
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
&
mcp
.
Tool
{
Name
:
"
list
_feeds"
,
Name
:
"
search
_feeds"
,
Description
:
"
获取首页 Feeds 列表
"
,
Description
:
"
搜索小红书内容(需要已登录)
"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"
List
Feeds"
,
Title
:
"
Search
Feeds"
,
ReadOnlyHint
:
true
,
ReadOnlyHint
:
true
,
},
},
},
},
withPanicRecovery
(
"
list_feeds"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
_
any
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
withPanicRecovery
(
"
search_feeds"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
args
SearchFeedsArgs
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handle
ListFeeds
(
ctx
)
result
:=
appServer
.
handle
SearchFeeds
(
ctx
,
args
)
return
convertToMCPResult
(
result
),
nil
,
nil
return
convertToMCPResult
(
result
),
nil
,
nil
}),
}),
)
)
//
工具 6: 搜索内容
//
获取我的主页
mcp
.
AddTool
(
server
,
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
&
mcp
.
Tool
{
Name
:
"
search_feeds
"
,
Name
:
"
get_my_profile
"
,
Description
:
"
搜索小红书内容(需要已登录)
"
,
Description
:
"
获取当前登录用户的主页,返回用户基本信息,关注、粉丝、获赞量,以及指定 tab 下的内容。tab 可选 note(自己发的笔记,默认)、fav(自己收藏的)、liked(自己点赞的)
"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"
Search Feeds
"
,
Title
:
"
Get My Profile
"
,
ReadOnlyHint
:
true
,
ReadOnlyHint
:
true
,
},
},
},
},
withPanicRecovery
(
"search_feeds"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
args
SearchFeedsArgs
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
withPanicRecovery
(
"get_my_profile"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
args
MyProfileArgs
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handleSearchFeeds
(
ctx
,
args
)
result
:=
appServer
.
handleGetMyProfile
(
ctx
,
args
.
Tab
)
return
convertToMCPResult
(
result
),
nil
,
nil
}),
)
if
appServer
.
publishOnly
{
logrus
.
Info
(
"Registered 4 publish-only MCP tools"
)
return
}
// 工具 2: 获取登录二维码
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
Name
:
"get_login_qrcode"
,
Description
:
"获取登录二维码(返回 Base64 图片和超时时间)"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"Get Login QR Code"
,
ReadOnlyHint
:
true
,
},
},
withPanicRecovery
(
"get_login_qrcode"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
_
any
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handleGetLoginQrcode
(
ctx
)
return
convertToMCPResult
(
result
),
nil
,
nil
}),
)
// 工具 3: 删除 cookies(登录重置)
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
Name
:
"delete_cookies"
,
Description
:
"删除 cookies 文件,重置登录状态。删除后需要重新登录。"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"Delete Cookies"
,
DestructiveHint
:
boolPtr
(
true
),
},
},
withPanicRecovery
(
"delete_cookies"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
_
any
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handleDeleteCookies
(
ctx
)
return
convertToMCPResult
(
result
),
nil
,
nil
}),
)
// 工具 5: 获取Feed列表
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
Name
:
"list_feeds"
,
Description
:
"获取首页 Feeds 列表"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"List Feeds"
,
ReadOnlyHint
:
true
,
},
},
withPanicRecovery
(
"list_feeds"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
_
any
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handleListFeeds
(
ctx
)
return
convertToMCPResult
(
result
),
nil
,
nil
return
convertToMCPResult
(
result
),
nil
,
nil
}),
}),
)
)
...
@@ -480,22 +501,6 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
...
@@ -480,22 +501,6 @@ func registerTools(server *mcp.Server, appServer *AppServer) {
}),
}),
)
)
// 工具 14: 获取我的主页
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
Name
:
"get_my_profile"
,
Description
:
"获取当前登录用户的主页,返回用户基本信息,关注、粉丝、获赞量,以及指定 tab 下的内容。tab 可选 note(自己发的笔记,默认)、fav(自己收藏的)、liked(自己点赞的)"
,
Annotations
:
&
mcp
.
ToolAnnotations
{
Title
:
"Get My Profile"
,
ReadOnlyHint
:
true
,
},
},
withPanicRecovery
(
"get_my_profile"
,
func
(
ctx
context
.
Context
,
req
*
mcp
.
CallToolRequest
,
args
MyProfileArgs
)
(
*
mcp
.
CallToolResult
,
any
,
error
)
{
result
:=
appServer
.
handleGetMyProfile
(
ctx
,
args
.
Tab
)
return
convertToMCPResult
(
result
),
nil
,
nil
}),
)
// 工具 15: 通知未读数
// 工具 15: 通知未读数
mcp
.
AddTool
(
server
,
mcp
.
AddTool
(
server
,
&
mcp
.
Tool
{
&
mcp
.
Tool
{
...
...
routes.go
View file @
86ed10c7
...
@@ -46,22 +46,26 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
...
@@ -46,22 +46,26 @@ func setupRoutes(appServer *AppServer) *gin.Engine {
// API 路由组
// API 路由组
api
:=
protected
.
Group
(
"/api/v1"
)
api
:=
protected
.
Group
(
"/api/v1"
)
{
api
.
GET
(
"/login/status"
,
appServer
.
checkLoginStatusHandler
)
api
.
GET
(
"/login/status"
,
appServer
.
checkLoginStatusHandler
)
api
.
POST
(
"/publish"
,
appServer
.
publishHandler
)
api
.
GET
(
"/feeds/search"
,
appServer
.
searchFeedsHandler
)
api
.
POST
(
"/feeds/search"
,
appServer
.
searchFeedsHandler
)
api
.
GET
(
"/user/me"
,
appServer
.
myProfileHandler
)
if
appServer
.
publishOnly
{
return
router
}
{
api
.
GET
(
"/login/qrcode"
,
appServer
.
getLoginQrcodeHandler
)
api
.
GET
(
"/login/qrcode"
,
appServer
.
getLoginQrcodeHandler
)
api
.
DELETE
(
"/login/cookies"
,
appServer
.
deleteCookiesHandler
)
api
.
DELETE
(
"/login/cookies"
,
appServer
.
deleteCookiesHandler
)
api
.
POST
(
"/publish"
,
appServer
.
publishHandler
)
api
.
POST
(
"/publish_video"
,
appServer
.
publishVideoHandler
)
api
.
POST
(
"/publish_video"
,
appServer
.
publishVideoHandler
)
api
.
GET
(
"/feeds/list"
,
appServer
.
listFeedsHandler
)
api
.
GET
(
"/feeds/list"
,
appServer
.
listFeedsHandler
)
api
.
GET
(
"/feeds/search"
,
appServer
.
searchFeedsHandler
)
api
.
POST
(
"/feeds/search"
,
appServer
.
searchFeedsHandler
)
api
.
POST
(
"/feeds/detail"
,
appServer
.
getFeedDetailHandler
)
api
.
POST
(
"/feeds/detail"
,
appServer
.
getFeedDetailHandler
)
api
.
POST
(
"/user/profile"
,
appServer
.
userProfileHandler
)
api
.
POST
(
"/user/profile"
,
appServer
.
userProfileHandler
)
api
.
POST
(
"/feeds/comment"
,
appServer
.
postCommentHandler
)
api
.
POST
(
"/feeds/comment"
,
appServer
.
postCommentHandler
)
api
.
POST
(
"/feeds/comment/reply"
,
appServer
.
replyCommentHandler
)
api
.
POST
(
"/feeds/comment/reply"
,
appServer
.
replyCommentHandler
)
api
.
POST
(
"/feeds/like"
,
appServer
.
likeFeedHandler
)
api
.
POST
(
"/feeds/like"
,
appServer
.
likeFeedHandler
)
api
.
POST
(
"/feeds/favorite"
,
appServer
.
favoriteFeedHandler
)
api
.
POST
(
"/feeds/favorite"
,
appServer
.
favoriteFeedHandler
)
api
.
GET
(
"/user/me"
,
appServer
.
myProfileHandler
)
api
.
GET
(
"/notifications/unread"
,
appServer
.
getUnreadCountHandler
)
api
.
GET
(
"/notifications/unread"
,
appServer
.
getUnreadCountHandler
)
api
.
GET
(
"/notifications/list"
,
appServer
.
listNotificationsHandler
)
api
.
GET
(
"/notifications/list"
,
appServer
.
listNotificationsHandler
)
api
.
POST
(
"/notifications/list"
,
appServer
.
listNotificationsHandler
)
api
.
POST
(
"/notifications/list"
,
appServer
.
listNotificationsHandler
)
...
...
routes_test.go
View file @
86ed10c7
...
@@ -205,3 +205,49 @@ func TestMCPRejectsNullJSONKey(t *testing.T) {
...
@@ -205,3 +205,49 @@ func TestMCPRejectsNullJSONKey(t *testing.T) {
assert
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
,
"含 NUL 的 JSON 键应被拒绝"
)
assert
.
Equal
(
t
,
http
.
StatusBadRequest
,
recorder
.
Code
,
"含 NUL 的 JSON 键应被拒绝"
)
}
}
func
TestPublishOnlySurface
(
t
*
testing
.
T
)
{
router
:=
setupRoutes
(
newAppServer
(
NewXiaohongshuService
(),
"test-token"
,
true
))
listTools
:=
func
(
token
string
)
*
httptest
.
ResponseRecorder
{
req
:=
httptest
.
NewRequest
(
http
.
MethodPost
,
"/mcp"
,
strings
.
NewReader
(
`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`
))
req
.
Header
.
Set
(
"Content-Type"
,
"application/json"
)
req
.
Header
.
Set
(
"Accept"
,
"application/json, text/event-stream"
)
if
token
!=
""
{
req
.
Header
.
Set
(
"Authorization"
,
"Bearer "
+
token
)
}
resp
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
resp
,
req
)
return
resp
}
require
.
Equal
(
t
,
http
.
StatusUnauthorized
,
listTools
(
""
)
.
Code
)
resp
:=
listTools
(
"test-token"
)
require
.
Equal
(
t
,
http
.
StatusOK
,
resp
.
Code
)
var
result
struct
{
Result
struct
{
Tools
[]
struct
{
Name
string
`json:"name"`
}
`json:"tools"`
}
`json:"result"`
}
require
.
NoError
(
t
,
json
.
NewDecoder
(
resp
.
Body
)
.
Decode
(
&
result
))
names
:=
make
([]
string
,
0
,
len
(
result
.
Result
.
Tools
))
for
_
,
tool
:=
range
result
.
Result
.
Tools
{
names
=
append
(
names
,
tool
.
Name
)
}
assert
.
ElementsMatch
(
t
,
[]
string
{
"check_login_status"
,
"publish_content"
,
"get_my_profile"
,
"search_feeds"
},
names
)
for
_
,
route
:=
range
[]
struct
{
method
,
path
string
}{
{
http
.
MethodGet
,
"/api/v1/login/qrcode"
},
{
http
.
MethodDelete
,
"/api/v1/login/cookies"
},
{
http
.
MethodPost
,
"/api/v1/publish_video"
},
{
http
.
MethodPost
,
"/api/v1/feeds/comment"
},
{
http
.
MethodGet
,
"/api/v1/notifications/list"
},
}
{
req
:=
httptest
.
NewRequest
(
route
.
method
,
route
.
path
,
nil
)
req
.
Header
.
Set
(
"Authorization"
,
"Bearer test-token"
)
resp
:=
httptest
.
NewRecorder
()
router
.
ServeHTTP
(
resp
,
req
)
assert
.
Equal
(
t
,
http
.
StatusNotFound
,
resp
.
Code
,
route
.
path
)
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment