@wenyan-md/mcp 1.0.10 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -91
- package/dist/index.js +47 -131
- package/dist/mcpServer.js +93 -0
- package/dist/publish.js +48 -0
- package/dist/theme.js +38 -0
- package/dist/types/index.d.ts +0 -8
- package/dist/types/mcpServer.d.ts +5 -0
- package/dist/types/publish.d.ts +31 -0
- package/dist/types/theme.d.ts +38 -0
- package/dist/types/types.d.ts +8 -0
- package/dist/types/utils.d.ts +23 -1
- package/dist/types.js +1 -0
- package/dist/utils.js +47 -23
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img alt
|
|
2
|
+
<img alt="logo" src="https://media.githubusercontent.com/media/caol64/wenyan-mcp/main/data/wenyan-mcp.png" width="256" />
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
5
|
# 文颜 MCP Server
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
## 简介
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**[文颜(Wenyan)](https://wenyan.yuzhi.tech)** 是一款多平台 Markdown 排版与发布工具,支持将 Markdown 一键转换并发布至:
|
|
16
16
|
|
|
17
17
|
- 微信公众号
|
|
18
18
|
- 知乎
|
|
@@ -21,8 +21,6 @@
|
|
|
21
21
|
|
|
22
22
|
文颜的目标是:**让写作者专注内容,而不是排版和平台适配**。
|
|
23
23
|
|
|
24
|
-
本仓库是 **文颜的 MCP Server 版本**,基于模型上下文协议(Model Context Protocol),旨在让 AI 助手(如 Claude Desktop)具备自动排版和发布公众号文章的能力。
|
|
25
|
-
|
|
26
24
|
## 文颜的不同版本
|
|
27
25
|
|
|
28
26
|
文颜目前提供多种形态,覆盖不同使用场景:
|
|
@@ -31,57 +29,50 @@
|
|
|
31
29
|
- [跨平台桌面版](https://github.com/caol64/wenyan-pc) - Windows/Linux
|
|
32
30
|
- [CLI 版本](https://github.com/caol64/wenyan-cli) - 命令行 / CI 自动化发布
|
|
33
31
|
- 👉 [MCP 版本](https://github.com/caol64/wenyan-mcp) - 本项目
|
|
34
|
-
- [核心库](https://github.com/caol64/wenyan-core) - 嵌入 Node / Web 项目
|
|
35
|
-
|
|
36
|
-
## 功能特性
|
|
37
|
-
|
|
38
|
-
- 列出并选择支持的文章主题
|
|
39
|
-
- 使用内置主题对 Markdown 内容排版
|
|
40
|
-
- 自动处理并上传图片(本地 / 网络)
|
|
41
|
-
- 一键发布文章到微信公众号草稿箱
|
|
42
|
-
- **与 AI 深度集成**:[让 AI 帮你管理公众号的排版和发布](https://babyno.top/posts/2025/06/let-ai-help-you-manage-your-gzh-layout-and-publishing/)
|
|
43
32
|
|
|
44
|
-
|
|
33
|
+
## 文颜 MCP Server 是什么?
|
|
45
34
|
|
|
46
|
-
|
|
47
|
-
>
|
|
48
|
-
> 如果与 AI 集成遇到问题,可以参考 [test/list.js](./test/list.js) 和 [test/publish.js](./test/publish.js) 中的完整调用示例。
|
|
35
|
+
简单来说,它打通了“AI 写作”与“公众号发文”的通道。
|
|
49
36
|
|
|
50
|
-
|
|
37
|
+
基于 MCP 协议,Claude Desktop 等 AI 客户端现在可以直接调用文颜(Wenyan)的排版引擎。写完文章后,不需要再去第三方编辑器里来回复制粘贴,直接让 AI 帮你排版并塞进微信草稿箱。
|
|
51
38
|
|
|
52
|
-
|
|
39
|
+
**核心特性:**
|
|
53
40
|
|
|
54
|
-
|
|
41
|
+
- **绕过排版工具**:AI 生成的 Markdown 直接转成微信富文本并上传,省去中间步骤。
|
|
42
|
+
- **对话式排版**:直接打字跟 AI 说“换个橙色风格主题”,样式自动生效。
|
|
43
|
+
- **不出窗口完成闭环**:在同一个聊天框里,顺滑搞定“想选题 -> 写文章 -> 调排版 -> 存草稿”的所有操作。
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
- [Pie](https://github.com/kevinzhao2233/typora-theme-pie)
|
|
60
|
-
- [Maize](https://github.com/BEATREE/typora-maize-theme)
|
|
61
|
-
- [Purple](https://github.com/hliu202/typora-purple-theme)
|
|
62
|
-
- [物理猫-薄荷](https://github.com/sumruler/typora-theme-phycat)
|
|
45
|
+
**实战演示**:
|
|
46
|
+
* [让 AI 帮你管理公众号的排版和发布](https://babyno.top/posts/2025/06/let-ai-help-you-manage-your-gzh-layout-and-publishing/)
|
|
47
|
+
* [Moraya MCP 使用案例:微信公众号全托管](https://github.com/zouwei/moraya/wiki/Moraya-MCP-%E4%BD%BF%E7%94%A8%E6%A1%88%E4%BE%8B%EF%BC%9A%E5%BE%AE%E4%BF%A1%E5%85%AC%E4%BC%97%E5%8F%B7%E5%85%A8%E6%89%98%E7%AE%A1)
|
|
63
48
|
|
|
64
|
-
##
|
|
49
|
+
## 功能特性
|
|
65
50
|
|
|
66
|
-
|
|
51
|
+
- 一键发布 Markdown 到微信公众号草稿箱
|
|
52
|
+
- 自动上传本地图片与封面
|
|
53
|
+
- 支持远程 Server 发布(绕过 IP 白名单限制)
|
|
54
|
+
- 内置多套精美排版主题
|
|
55
|
+
- 支持自定义主题
|
|
56
|
+
- 提供标准 MCP Tool 接口
|
|
57
|
+
- 支持 AI 自动调用:
|
|
58
|
+
- 渲染 Markdown
|
|
59
|
+
- 主题管理
|
|
60
|
+
- 发布草稿
|
|
67
61
|
|
|
68
|
-
|
|
62
|
+
## 快速开始
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
**安装**
|
|
71
65
|
|
|
72
66
|
```bash
|
|
73
67
|
npm install -g @wenyan-md/mcp
|
|
74
68
|
```
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
在你的 MCP 配置文件中加入以下内容:
|
|
70
|
+
**Claude Desktop 配置 (claude_desktop_config.json):**:
|
|
79
71
|
|
|
80
72
|
```json
|
|
81
73
|
{
|
|
82
74
|
"mcpServers": {
|
|
83
75
|
"wenyan-mcp": {
|
|
84
|
-
"name": "公众号助手",
|
|
85
76
|
"command": "wenyan-mcp",
|
|
86
77
|
"env": {
|
|
87
78
|
"WECHAT_APP_ID": "your_app_id",
|
|
@@ -92,60 +83,103 @@ npm install -g @wenyan-md/mcp
|
|
|
92
83
|
}
|
|
93
84
|
```
|
|
94
85
|
|
|
95
|
-
|
|
86
|
+
## 基本用法
|
|
96
87
|
|
|
97
|
-
|
|
88
|
+
### 列出主题
|
|
98
89
|
|
|
99
|
-
|
|
90
|
+
对AI说
|
|
91
|
+
|
|
92
|
+
> 目前你可以使用哪些公众号主题?
|
|
93
|
+
|
|
94
|
+
AI回复:
|
|
100
95
|
|
|
101
|
-
```bash
|
|
102
|
-
docker pull caol64/wenyan-mcp
|
|
103
96
|
```
|
|
97
|
+
目前我支持的公众号主题包括以下这些,每个主题都有独特的风格和配色,适用于不同的内容调性:
|
|
104
98
|
|
|
105
|
-
|
|
99
|
+
1. **Default(默认)**
|
|
100
|
+
简洁经典的排版,适合长文阅读,通用性强。
|
|
106
101
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
"wenyan-mcp": {
|
|
111
|
-
"name": "公众号助手",
|
|
112
|
-
"command": "docker",
|
|
113
|
-
"args": [
|
|
114
|
-
"run",
|
|
115
|
-
"--rm",
|
|
116
|
-
"-i",
|
|
117
|
-
"-v", "/your/host/file/path:/mnt/host-downloads",
|
|
118
|
-
"-e", "WECHAT_APP_ID=your_app_id",
|
|
119
|
-
"-e", "WECHAT_APP_SECRET=your_app_secret",
|
|
120
|
-
"-e", "HOST_FILE_PATH=/your/host/file/path",
|
|
121
|
-
"caol64/wenyan-mcp"
|
|
122
|
-
]
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
102
|
+
2. **OrangeHeart(橙心)**
|
|
103
|
+
暖橙色调,优雅而富有活力,适合情感、生活类内容。
|
|
104
|
+
...
|
|
126
105
|
```
|
|
127
106
|
|
|
128
|
-
|
|
107
|
+
### 注册主题
|
|
108
|
+
|
|
109
|
+
对AI说
|
|
110
|
+
|
|
111
|
+
> 帮我把这个css(https://wenyan.yuzhi.tech/manhua.css)注册为一个新的公众号主题,名称为:xiuluochang
|
|
112
|
+
|
|
113
|
+
AI回复:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
好的,已成功将 CSS 样式表注册为新的公众号主题,主题名称为:**xiuluochang**。
|
|
117
|
+
|
|
118
|
+
您现在可以在公众号后台选择该主题进行应用,样式将按照 `https://wenyan.yuzhi.tech/manhua.css` 的定义进行渲染。
|
|
119
|
+
|
|
120
|
+
如有需要进一步自定义或更换主题,随时告诉我!
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 删除主题
|
|
124
|
+
|
|
125
|
+
对AI说
|
|
126
|
+
|
|
127
|
+
> 把名为“xiuluochang”的公众号主题删除
|
|
128
|
+
|
|
129
|
+
AI回复:
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
公众号主题 “xiuluochang” 已成功删除。
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 发布文章
|
|
136
|
+
|
|
137
|
+
对AI说
|
|
138
|
+
|
|
139
|
+
> 使用phycat主题将这篇文章发布到微信公众号:`./tests/publish.md`
|
|
140
|
+
|
|
141
|
+
AI回复:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
文章已成功发布至微信公众号草稿箱!🎉
|
|
145
|
+
|
|
146
|
+
- **主题**:phycat
|
|
147
|
+
- **媒体ID**:xxx
|
|
148
|
+
|
|
149
|
+
您可登录微信公众号后台,在「草稿箱」中查看并编辑文章,确认无误后即可一键发布。如需进一步排版优化、添加封面或设置摘要,我也可以协助您完成!
|
|
150
|
+
|
|
151
|
+
是否需要我帮您生成一篇发布文案或封面建议? 😊
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 概念
|
|
155
|
+
|
|
156
|
+
### 环境变量配置
|
|
157
|
+
|
|
158
|
+
> [!IMPORTANT]
|
|
129
159
|
>
|
|
130
|
-
>
|
|
131
|
-
> * **环境变量 (`HOST_FILE_PATH`)**:必须与宿主机挂载的文件/图片目录路径保持一致。
|
|
132
|
-
> * **原理**:你的 Markdown 文件/文章内所引用的本地图片应放置在该目录中,Docker 会自动将其映射,使容器能够读取并上传。
|
|
160
|
+
> 请确保 MCP 启动时已配置如下环境变量,否则上传接口将调用失败。
|
|
133
161
|
|
|
134
|
-
|
|
162
|
+
- `WECHAT_APP_ID`
|
|
163
|
+
- `WECHAT_APP_SECRET`
|
|
135
164
|
|
|
136
|
-
|
|
165
|
+
### 微信公众号 IP 白名单
|
|
137
166
|
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
> [!IMPORTANT]
|
|
168
|
+
>
|
|
169
|
+
> 请确保运行文颜的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
|
|
170
|
+
|
|
171
|
+
配置说明文档:[https://yuzhi.tech/docs/wenyan/upload](https://yuzhi.tech/docs/wenyan/upload)
|
|
140
172
|
|
|
141
|
-
|
|
173
|
+
### 文章格式
|
|
142
174
|
|
|
143
|
-
为了正确上传文章,每篇 Markdown
|
|
175
|
+
为了正确上传文章,每篇 Markdown 顶部需要包含一段 `frontmatter`:
|
|
144
176
|
|
|
145
177
|
```md
|
|
146
178
|
---
|
|
147
179
|
title: 在本地跑一个大语言模型(2) - 给模型提供外部知识库
|
|
148
180
|
cover: /Users/xxx/image.jpg
|
|
181
|
+
author: xxx
|
|
182
|
+
source_url: http://
|
|
149
183
|
---
|
|
150
184
|
```
|
|
151
185
|
|
|
@@ -154,41 +188,100 @@ cover: /Users/xxx/image.jpg
|
|
|
154
188
|
- `title` 文章标题(必填)
|
|
155
189
|
- `cover` 文章封面
|
|
156
190
|
- 本地路径或网络图片
|
|
157
|
-
-
|
|
158
|
-
|
|
191
|
+
- 如果正文中已有图片,可省略
|
|
192
|
+
- `author` 文章作者
|
|
193
|
+
- `source_url` 原文地址
|
|
159
194
|
|
|
160
|
-
|
|
195
|
+
**[示例文章](tests/publish.md)**
|
|
161
196
|
|
|
162
|
-
|
|
197
|
+
### 文内图片和文章封面
|
|
163
198
|
|
|
164
|
-
|
|
165
|
-
- 网络路径(如:`https://example.com/image.jpg`)
|
|
199
|
+
把文章发布到公众号之前,文颜会按照微信要求自动处理文章内的所有图片,将其上传到公众号素材库。目前文颜对于以下图片都能很好的支持:
|
|
166
200
|
|
|
167
|
-
|
|
201
|
+
- 本地硬盘绝对路径(如:`/Users/xxx/image.jpg`)
|
|
202
|
+
- 网络路径(如:`https://example.com/image.jpg`)
|
|
203
|
+
- 当前文章的相对路径(如:`./assets/image.png`)
|
|
168
204
|
|
|
169
|
-
|
|
170
|
-
>
|
|
171
|
-
> 请确保运行文颜 MCP Server 的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
|
|
205
|
+
## Server 模式
|
|
172
206
|
|
|
173
|
-
|
|
207
|
+
相较于纯本地运行的**本地模式(Stdio Mode)**,`wenyan-mcp`还提供了 **远程客户端模式(Client–Server Mode)**。两种模式运行效果完全一致,你可以根据运行环境和网络条件选择最合适的方式。
|
|
174
208
|
|
|
175
|
-
|
|
209
|
+
在本地模式下,MCP 直接调用微信公众号 API 完成图片上传和草稿发布。
|
|
176
210
|
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
211
|
+
```mermaid
|
|
212
|
+
flowchart LR
|
|
213
|
+
MCP[Wenyan MCP] --> Wechat[公众号 API]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
在远程客户端模式下,MCP 作为客户端,将发布请求发送到部署在云服务器上的 Wenyan Server,由 Server 完成微信公众号 API 调用。
|
|
217
|
+
|
|
218
|
+
```mermaid
|
|
219
|
+
flowchart LR
|
|
220
|
+
MCP[Wenyan MCP] --> Server[Wenyan Server] --> Wechat[公众号 API]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
**适用于:**
|
|
224
|
+
|
|
225
|
+
* 无本地固定 IP,需频繁添加IP 白名单的用户
|
|
226
|
+
* 需团队协作的用户
|
|
227
|
+
* 支持 CI/CD 自动发布
|
|
228
|
+
* 支持 AI Agent 自动发布
|
|
229
|
+
|
|
230
|
+
**[Server 模式部署](https://github.com/caol64/wenyan-cli/blob/main/docs/server.md)**
|
|
231
|
+
|
|
232
|
+
**Claude Desktop 配置:**:
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"mcpServers": {
|
|
237
|
+
"wenyan-mcp": {
|
|
238
|
+
"command": "wenyan-mcp",
|
|
239
|
+
"args": ["--server", "https://api.example.com", "--api-key", "your-api-key"]
|
|
240
|
+
"env": {
|
|
241
|
+
"WECHAT_APP_ID": "your_app_id",
|
|
242
|
+
"WECHAT_APP_SECRET": "your_app_secret"
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Docker 部署
|
|
182
250
|
|
|
183
|
-
|
|
251
|
+
适合不希望安装 Node.js 环境的用户。
|
|
184
252
|
|
|
185
|
-
|
|
253
|
+
```bash
|
|
254
|
+
docker pull caol64/wenyan-mcp:latest
|
|
255
|
+
```
|
|
186
256
|
|
|
187
|
-
|
|
257
|
+
* **Claude Desktop 配置:**:
|
|
188
258
|
|
|
189
|
-
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"mcpServers": {
|
|
262
|
+
"wenyan-mcp": {
|
|
263
|
+
"command": "docker",
|
|
264
|
+
"args": [
|
|
265
|
+
"run",
|
|
266
|
+
"--rm",
|
|
267
|
+
"-i",
|
|
268
|
+
"-v", "/your/host/file/path:/mnt/host-downloads",
|
|
269
|
+
"-e", "WECHAT_APP_ID=your_app_id",
|
|
270
|
+
"-e", "WECHAT_APP_SECRET=your_app_secret",
|
|
271
|
+
"-e", "HOST_FILE_PATH=/your/host/file/path",
|
|
272
|
+
"caol64/wenyan-mcp"
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
190
277
|
```
|
|
191
278
|
|
|
279
|
+
> **Docker 配置特别说明:**
|
|
280
|
+
>
|
|
281
|
+
> * **挂载目录 (`-v`)**:必须将宿主机的文件/图片目录挂载到容器内的 `/mnt/host-downloads`。
|
|
282
|
+
> * **环境变量 (`HOST_FILE_PATH`)**:必须与宿主机挂载的文件/图片目录路径保持一致。
|
|
283
|
+
> * **原理**:你的 Markdown 文件/文章内所引用的本地图片应放置在该目录中,Docker 会自动将其映射,使容器能够读取并上传。
|
|
284
|
+
|
|
192
285
|
## 如何调试
|
|
193
286
|
|
|
194
287
|
推荐使用官方 Inspector 进行调试:
|
package/dist/index.js
CHANGED
|
@@ -1,145 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* This is a template MCP server that implements a simple notes system.
|
|
4
|
-
* It demonstrates core MCP concepts like resources and tools by allowing:
|
|
5
|
-
* - Listing notes as resources
|
|
6
|
-
* - Reading individual notes
|
|
7
|
-
* - Creating new notes via a tool
|
|
8
|
-
* - Summarizing all notes via a prompt
|
|
9
|
-
*/
|
|
10
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { publishToDraft } from "@wenyan-md/core/publish";
|
|
15
|
-
import { themes } from "@wenyan-md/core/theme";
|
|
16
|
-
import { getNormalizeFilePath } from "./utils.js";
|
|
17
|
-
import fs from "node:fs/promises";
|
|
18
|
-
import path from "node:path";
|
|
3
|
+
import { createServer } from "./mcpServer.js";
|
|
4
|
+
import { globalStates } from "./utils.js";
|
|
19
5
|
/**
|
|
20
|
-
*
|
|
21
|
-
* tools (to create new notes), and prompts (to summarize notes).
|
|
22
|
-
*/
|
|
23
|
-
const server = new Server({
|
|
24
|
-
name: "wenyan-mcp",
|
|
25
|
-
version: "0.1.0",
|
|
26
|
-
}, {
|
|
27
|
-
capabilities: {
|
|
28
|
-
resources: {},
|
|
29
|
-
tools: {},
|
|
30
|
-
prompts: {},
|
|
31
|
-
// logging: {},
|
|
32
|
-
},
|
|
33
|
-
});
|
|
34
|
-
/**
|
|
35
|
-
* Handler that lists available tools.
|
|
36
|
-
* Exposes a single "publish_article" tool that lets clients publish new article.
|
|
6
|
+
* Start the server using stdio transport.
|
|
37
7
|
*/
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
inputSchema: {
|
|
45
|
-
type: "object",
|
|
46
|
-
properties: {
|
|
47
|
-
content: {
|
|
48
|
-
type: "string",
|
|
49
|
-
description: "The Markdown text to publish. REQUIRED if 'file' is not provided. Preserves frontmatter if present.",
|
|
50
|
-
},
|
|
51
|
-
file: {
|
|
52
|
-
type: "string",
|
|
53
|
-
description: "The path to the Markdown file (absolute or relative). REQUIRED if 'content' is not provided.",
|
|
54
|
-
},
|
|
55
|
-
theme_id: {
|
|
56
|
-
type: "string",
|
|
57
|
-
description: "ID of the theme to use (e.g., default, orangeheart, rainbow, lapis, pie, maize, purple, phycat).",
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "list_themes",
|
|
64
|
-
description: "List the themes compatible with the 'publish_article' tool to publish an article to '微信公众号'.",
|
|
65
|
-
inputSchema: {
|
|
66
|
-
type: "object",
|
|
67
|
-
properties: {},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
};
|
|
72
|
-
});
|
|
8
|
+
async function mainStdio() {
|
|
9
|
+
const server = createServer();
|
|
10
|
+
const transport = new StdioServerTransport();
|
|
11
|
+
await server.connect(transport);
|
|
12
|
+
console.error("[Init] Wenyan MCP server started successfully and listening for requests...");
|
|
13
|
+
}
|
|
73
14
|
/**
|
|
74
|
-
*
|
|
75
|
-
* Publish a new article with the provided title and content, and returns success message.
|
|
15
|
+
* Main entry point: parse command line arguments and start appropriate transport.
|
|
76
16
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new Error("You must provide either 'content' or 'file' to publish an article.");
|
|
87
|
-
}
|
|
88
|
-
let content = String(contentArg || "");
|
|
89
|
-
const file = String(fileArg || "");
|
|
90
|
-
const themeId = String(request.params.arguments?.theme_id || "");
|
|
91
|
-
let absoluteDirPath;
|
|
92
|
-
if (!content && file) {
|
|
93
|
-
const normalizePath = getNormalizeFilePath(file);
|
|
94
|
-
content = await fs.readFile(normalizePath, "utf-8");
|
|
95
|
-
if (!content) {
|
|
96
|
-
throw new Error("Can't read content from the specified file.");
|
|
97
|
-
}
|
|
98
|
-
absoluteDirPath = path.dirname(normalizePath);
|
|
99
|
-
}
|
|
100
|
-
const gzhContent = await getGzhContent(content, themeId, "solarized-light", true, true);
|
|
101
|
-
if (!gzhContent.title) {
|
|
102
|
-
throw new Error("Can't extract a valid title from the frontmatter.");
|
|
17
|
+
async function main() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const isClientMode = args.includes("--server");
|
|
20
|
+
if (isClientMode) {
|
|
21
|
+
globalStates.isClientMode = true;
|
|
22
|
+
console.error("[Init] Starting Wenyan MCP server in remote client mode...");
|
|
23
|
+
const serverIndex = args.indexOf("--server");
|
|
24
|
+
if (serverIndex !== -1 && args[serverIndex + 1] && !args[serverIndex + 1].startsWith("--")) {
|
|
25
|
+
globalStates.serverUrl = args[serverIndex + 1];
|
|
103
26
|
}
|
|
104
|
-
|
|
105
|
-
|
|
27
|
+
const apiKeyIndex = args.indexOf("--api-key");
|
|
28
|
+
if (apiKeyIndex !== -1 && args[apiKeyIndex + 1] && !args[apiKeyIndex + 1].startsWith("--")) {
|
|
29
|
+
globalStates.apiKey = args[apiKeyIndex + 1];
|
|
106
30
|
}
|
|
107
|
-
const response = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover, {
|
|
108
|
-
relativePath: absoluteDirPath,
|
|
109
|
-
});
|
|
110
|
-
return {
|
|
111
|
-
content: [
|
|
112
|
-
{
|
|
113
|
-
type: "text",
|
|
114
|
-
text: `Your article was successfully published to '公众号草稿箱'. The media ID is ${response.media_id}.`,
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
};
|
|
118
31
|
}
|
|
119
|
-
else
|
|
120
|
-
|
|
121
|
-
type: "text",
|
|
122
|
-
text: JSON.stringify({
|
|
123
|
-
id: theme.id,
|
|
124
|
-
name: theme.name,
|
|
125
|
-
description: theme.description,
|
|
126
|
-
}),
|
|
127
|
-
}));
|
|
128
|
-
return {
|
|
129
|
-
content: themeResources,
|
|
130
|
-
};
|
|
32
|
+
else {
|
|
33
|
+
console.error("[Init] Starting Wenyan MCP server in local mode...");
|
|
131
34
|
}
|
|
132
|
-
|
|
133
|
-
});
|
|
134
|
-
/**
|
|
135
|
-
* Start the server using stdio transport.
|
|
136
|
-
* This allows the server to communicate via standard input/output streams.
|
|
137
|
-
*/
|
|
138
|
-
async function main() {
|
|
139
|
-
const transport = new StdioServerTransport();
|
|
140
|
-
await server.connect(transport);
|
|
35
|
+
await mainStdio();
|
|
141
36
|
}
|
|
37
|
+
// ==========================================
|
|
38
|
+
// 全局异常与信号处理 (Graceful Shutdown)
|
|
39
|
+
// ==========================================
|
|
142
40
|
main().catch((error) => {
|
|
143
|
-
console.error
|
|
41
|
+
// 必须使用 console.error 输出到 stderr,防止污染 MCP 的 JSON-RPC stdout 通道
|
|
42
|
+
console.error("[Fatal Error] Server initialization failed:", error.message);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
process.on("unhandledRejection", (reason) => {
|
|
46
|
+
console.error("[Unhandled Rejection] An unexpected error occurred:", reason?.message || reason);
|
|
47
|
+
// 注意:在 MCP 中通常不建议直接 exit,记录日志即可,让 Host 决定是否重启
|
|
48
|
+
});
|
|
49
|
+
process.on("uncaughtException", (error) => {
|
|
50
|
+
console.error("[Uncaught Exception] Critical error:", error.message);
|
|
144
51
|
process.exit(1);
|
|
145
52
|
});
|
|
53
|
+
// 3. 优雅退出
|
|
54
|
+
process.on("SIGINT", () => {
|
|
55
|
+
console.error("[Shutdown] Received SIGINT, exiting...");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
process.on("SIGTERM", () => {
|
|
59
|
+
console.error("[Shutdown] Received SIGTERM, exiting...");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { LIST_THEMES_SCHEMA, REGISTER_THEME_SCHEMA, REMOVE_THEME_SCHEMA } from "./theme.js";
|
|
4
|
+
import { PUBLISH_ARTICLE_SCHEMA, publishArticle } from "./publish.js";
|
|
5
|
+
import pkg from "../package.json" with { type: "json" };
|
|
6
|
+
import { buildMcpResponse, globalStates } from "./utils.js";
|
|
7
|
+
import { addTheme, listThemes, removeTheme } from "@wenyan-md/core/wrapper";
|
|
8
|
+
/**
|
|
9
|
+
* Create and configure an MCP server instance.
|
|
10
|
+
*/
|
|
11
|
+
export function createServer() {
|
|
12
|
+
const server = new Server({
|
|
13
|
+
name: "wenyan-mcp",
|
|
14
|
+
version: pkg.version,
|
|
15
|
+
}, {
|
|
16
|
+
capabilities: {
|
|
17
|
+
resources: {},
|
|
18
|
+
tools: {},
|
|
19
|
+
prompts: {},
|
|
20
|
+
// logging: {},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Handler that lists available tools.
|
|
25
|
+
* Exposes a single "publish_article" tool that lets clients publish new article.
|
|
26
|
+
*/
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
28
|
+
return {
|
|
29
|
+
tools: [PUBLISH_ARTICLE_SCHEMA, LIST_THEMES_SCHEMA, REGISTER_THEME_SCHEMA, REMOVE_THEME_SCHEMA],
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Handler for the publish_article tool.
|
|
34
|
+
* Publish a new article with the provided title and content, and returns success message.
|
|
35
|
+
*/
|
|
36
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
37
|
+
try {
|
|
38
|
+
if (request.params.name === "publish_article") {
|
|
39
|
+
if (globalStates.isClientMode && !globalStates.serverUrl) {
|
|
40
|
+
throw new Error("Missing server URL. Usage: --server <server_url>");
|
|
41
|
+
}
|
|
42
|
+
const args = request.params.arguments || {};
|
|
43
|
+
const content = String(args.content || "");
|
|
44
|
+
const contentUrl = String(args.content_url || "");
|
|
45
|
+
const file = String(args.file || "");
|
|
46
|
+
const themeId = String(args.theme_id || "");
|
|
47
|
+
return await publishArticle(contentUrl, file, content, themeId, pkg.version);
|
|
48
|
+
}
|
|
49
|
+
else if (request.params.name === "list_themes") {
|
|
50
|
+
const themes = await listThemes();
|
|
51
|
+
const builtinThemes = themes.filter((theme) => theme.isBuiltin);
|
|
52
|
+
const customThemes = themes.filter((theme) => !theme.isBuiltin);
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
...builtinThemes.map((theme) => ({
|
|
56
|
+
type: "text",
|
|
57
|
+
text: JSON.stringify({
|
|
58
|
+
id: theme.id,
|
|
59
|
+
name: theme.name,
|
|
60
|
+
description: theme.description,
|
|
61
|
+
}),
|
|
62
|
+
})),
|
|
63
|
+
...customThemes.map((theme) => ({
|
|
64
|
+
type: "text",
|
|
65
|
+
text: JSON.stringify({
|
|
66
|
+
id: theme.id,
|
|
67
|
+
name: theme.name ?? theme.id,
|
|
68
|
+
description: theme.description ?? "自定义主题,暂无描述。",
|
|
69
|
+
}),
|
|
70
|
+
})),
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
else if (request.params.name === "register_theme") {
|
|
75
|
+
const name = String(request.params.arguments?.name || "");
|
|
76
|
+
const path = String(request.params.arguments?.path || "");
|
|
77
|
+
await addTheme(name, path);
|
|
78
|
+
return buildMcpResponse(`Theme "${name}" has been added successfully.`);
|
|
79
|
+
}
|
|
80
|
+
else if (request.params.name === "remove_theme") {
|
|
81
|
+
const name = String(request.params.arguments?.name || "");
|
|
82
|
+
await removeTheme(name);
|
|
83
|
+
return buildMcpResponse(`Theme "${name}" has been removed successfully.`);
|
|
84
|
+
}
|
|
85
|
+
throw new Error("Unknown tool");
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.error(`[MCP Tool Error] (${request.params.name}):`, error.message);
|
|
89
|
+
return buildMcpResponse(`执行工具失败: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return server;
|
|
93
|
+
}
|
package/dist/publish.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { renderAndPublish, renderAndPublishToServer } from "@wenyan-md/core/wrapper";
|
|
2
|
+
import { buildMcpResponse, getInputContent, globalStates } from "./utils.js";
|
|
3
|
+
export const PUBLISH_ARTICLE_SCHEMA = {
|
|
4
|
+
name: "publish_article",
|
|
5
|
+
description: "Format a Markdown article using a selected theme and publish it to '微信公众号'.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: "object",
|
|
8
|
+
properties: {
|
|
9
|
+
content: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: "The Markdown text to publish. REQUIRED if 'file' or 'content_url' is not provided. DO INCLUDE frontmatter if present.",
|
|
12
|
+
},
|
|
13
|
+
content_url: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "A URL (e.g. GitHub raw link) to a Markdown file. Preferred over 'content' for large files to save tokens.",
|
|
16
|
+
},
|
|
17
|
+
file: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "The local path (absolute or relative) to a Markdown file. Preferred over 'content' for large files to save tokens.",
|
|
20
|
+
},
|
|
21
|
+
theme_id: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "ID of the theme to use (e.g., default, orangeheart, rainbow, lapis, pie, maize, purple, phycat).",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
export async function publishArticle(contentUrl, file, content, themeId, clientVersion) {
|
|
29
|
+
let mediaId = "";
|
|
30
|
+
const publishOptions = {
|
|
31
|
+
file: file ? file : contentUrl,
|
|
32
|
+
theme: themeId,
|
|
33
|
+
highlight: "solarized-light",
|
|
34
|
+
macStyle: true,
|
|
35
|
+
footnote: true,
|
|
36
|
+
server: globalStates.serverUrl,
|
|
37
|
+
apiKey: globalStates.apiKey,
|
|
38
|
+
clientVersion,
|
|
39
|
+
disableStdin: true,
|
|
40
|
+
};
|
|
41
|
+
if (globalStates.isClientMode) {
|
|
42
|
+
mediaId = await renderAndPublishToServer(content, publishOptions, getInputContent);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
mediaId = await renderAndPublish(content, publishOptions, getInputContent);
|
|
46
|
+
}
|
|
47
|
+
return buildMcpResponse(`Your article was successfully published to '公众号草稿箱'. The media ID is ${mediaId}.`);
|
|
48
|
+
}
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const LIST_THEMES_SCHEMA = {
|
|
2
|
+
name: "list_themes",
|
|
3
|
+
description: "List the themes compatible with the 'publish_article' tool to publish an article to '微信公众号'.",
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
properties: {},
|
|
7
|
+
},
|
|
8
|
+
};
|
|
9
|
+
export const REGISTER_THEME_SCHEMA = {
|
|
10
|
+
name: "register_theme",
|
|
11
|
+
description: "Register a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
name: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Name of the new custom theme.",
|
|
18
|
+
},
|
|
19
|
+
path: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Path to the new custom theme CSS file. It could be a path to a local file or a URL.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export const REMOVE_THEME_SCHEMA = {
|
|
27
|
+
name: "remove_theme",
|
|
28
|
+
description: "Remove a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
name: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "Name of the custom theme to remove.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,10 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* This is a template MCP server that implements a simple notes system.
|
|
4
|
-
* It demonstrates core MCP concepts like resources and tools by allowing:
|
|
5
|
-
* - Listing notes as resources
|
|
6
|
-
* - Reading individual notes
|
|
7
|
-
* - Creating new notes via a tool
|
|
8
|
-
* - Summarizing all notes via a prompt
|
|
9
|
-
*/
|
|
10
2
|
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const PUBLISH_ARTICLE_SCHEMA: {
|
|
2
|
+
readonly name: "publish_article";
|
|
3
|
+
readonly description: "Format a Markdown article using a selected theme and publish it to '微信公众号'.";
|
|
4
|
+
readonly inputSchema: {
|
|
5
|
+
readonly type: "object";
|
|
6
|
+
readonly properties: {
|
|
7
|
+
readonly content: {
|
|
8
|
+
readonly type: "string";
|
|
9
|
+
readonly description: "The Markdown text to publish. REQUIRED if 'file' or 'content_url' is not provided. DO INCLUDE frontmatter if present.";
|
|
10
|
+
};
|
|
11
|
+
readonly content_url: {
|
|
12
|
+
readonly type: "string";
|
|
13
|
+
readonly description: "A URL (e.g. GitHub raw link) to a Markdown file. Preferred over 'content' for large files to save tokens.";
|
|
14
|
+
};
|
|
15
|
+
readonly file: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
readonly description: "The local path (absolute or relative) to a Markdown file. Preferred over 'content' for large files to save tokens.";
|
|
18
|
+
};
|
|
19
|
+
readonly theme_id: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
readonly description: "ID of the theme to use (e.g., default, orangeheart, rainbow, lapis, pie, maize, purple, phycat).";
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export declare function publishArticle(contentUrl: string, file: string, content: string, themeId: string, clientVersion?: string): Promise<{
|
|
27
|
+
content: {
|
|
28
|
+
type: string;
|
|
29
|
+
text: string;
|
|
30
|
+
}[];
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const LIST_THEMES_SCHEMA: {
|
|
2
|
+
readonly name: "list_themes";
|
|
3
|
+
readonly description: "List the themes compatible with the 'publish_article' tool to publish an article to '微信公众号'.";
|
|
4
|
+
readonly inputSchema: {
|
|
5
|
+
readonly type: "object";
|
|
6
|
+
readonly properties: {};
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export declare const REGISTER_THEME_SCHEMA: {
|
|
10
|
+
readonly name: "register_theme";
|
|
11
|
+
readonly description: "Register a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.";
|
|
12
|
+
readonly inputSchema: {
|
|
13
|
+
readonly type: "object";
|
|
14
|
+
readonly properties: {
|
|
15
|
+
readonly name: {
|
|
16
|
+
readonly type: "string";
|
|
17
|
+
readonly description: "Name of the new custom theme.";
|
|
18
|
+
};
|
|
19
|
+
readonly path: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
readonly description: "Path to the new custom theme CSS file. It could be a path to a local file or a URL.";
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export declare const REMOVE_THEME_SCHEMA: {
|
|
27
|
+
readonly name: "remove_theme";
|
|
28
|
+
readonly description: "Remove a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.";
|
|
29
|
+
readonly inputSchema: {
|
|
30
|
+
readonly type: "object";
|
|
31
|
+
readonly properties: {
|
|
32
|
+
readonly name: {
|
|
33
|
+
readonly type: "string";
|
|
34
|
+
readonly description: "Name of the custom theme to remove.";
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -1 +1,23 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function buildMcpResponse(content: string): {
|
|
2
|
+
content: {
|
|
3
|
+
type: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}[];
|
|
6
|
+
};
|
|
7
|
+
declare class GlobalStates {
|
|
8
|
+
private _isClientMode;
|
|
9
|
+
private _serverUrl?;
|
|
10
|
+
private _apiKey?;
|
|
11
|
+
get isClientMode(): boolean;
|
|
12
|
+
set isClientMode(value: boolean);
|
|
13
|
+
get serverUrl(): string | undefined;
|
|
14
|
+
set serverUrl(value: string | undefined);
|
|
15
|
+
get apiKey(): string | undefined;
|
|
16
|
+
set apiKey(value: string | undefined);
|
|
17
|
+
}
|
|
18
|
+
export declare const globalStates: GlobalStates;
|
|
19
|
+
export declare function getInputContent(inputContent?: string, file?: string): Promise<{
|
|
20
|
+
content: string;
|
|
21
|
+
absoluteDirPath: string | undefined;
|
|
22
|
+
}>;
|
|
23
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.js
CHANGED
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { getNormalizeFilePath } from "@wenyan-md/core/wrapper";
|
|
4
|
+
export function buildMcpResponse(content) {
|
|
5
|
+
return {
|
|
6
|
+
content: [
|
|
7
|
+
{
|
|
8
|
+
type: "text",
|
|
9
|
+
text: content,
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
};
|
|
9
13
|
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (relativePart.startsWith(hostFilePath)) {
|
|
17
|
-
relativePart = relativePart.slice(hostFilePath.length);
|
|
18
|
-
}
|
|
19
|
-
if (!relativePart.startsWith("/")) {
|
|
20
|
-
relativePart = "/" + relativePart;
|
|
21
|
-
}
|
|
22
|
-
return containerFilePath + relativePart;
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
return path.resolve(inputPath);
|
|
14
|
+
class GlobalStates {
|
|
15
|
+
_isClientMode = false;
|
|
16
|
+
_serverUrl;
|
|
17
|
+
_apiKey;
|
|
18
|
+
get isClientMode() {
|
|
19
|
+
return this._isClientMode;
|
|
26
20
|
}
|
|
21
|
+
set isClientMode(value) {
|
|
22
|
+
this._isClientMode = value;
|
|
23
|
+
}
|
|
24
|
+
get serverUrl() {
|
|
25
|
+
return this._serverUrl;
|
|
26
|
+
}
|
|
27
|
+
set serverUrl(value) {
|
|
28
|
+
this._serverUrl = value;
|
|
29
|
+
}
|
|
30
|
+
get apiKey() {
|
|
31
|
+
return this._apiKey;
|
|
32
|
+
}
|
|
33
|
+
set apiKey(value) {
|
|
34
|
+
this._apiKey = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export const globalStates = new GlobalStates();
|
|
38
|
+
export async function getInputContent(inputContent, file) {
|
|
39
|
+
let absoluteDirPath = undefined;
|
|
40
|
+
// 2. 尝试从文件读取
|
|
41
|
+
if (!inputContent && file) {
|
|
42
|
+
const normalizePath = getNormalizeFilePath(file);
|
|
43
|
+
inputContent = await fs.readFile(normalizePath, "utf-8");
|
|
44
|
+
absoluteDirPath = path.dirname(normalizePath);
|
|
45
|
+
}
|
|
46
|
+
// 3. 校验输入
|
|
47
|
+
if (!inputContent) {
|
|
48
|
+
throw new Error("missing input-content (no argument, no stdin, and no file).");
|
|
49
|
+
}
|
|
50
|
+
return { content: inputContent, absoluteDirPath };
|
|
27
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenyan-md/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "MCP server for Wenyan, a Markdown formatting tool that allows AI assistants to apply elegant built-in themes and publish articles directly to 微信公众号.",
|
|
5
5
|
"author": "Lei <caol64@gmail.com> (https://github.com/caol64)",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "0.6.0",
|
|
34
|
-
"@wenyan-md/core": "^
|
|
34
|
+
"@wenyan-md/core": "^2.0.8",
|
|
35
|
+
"zod": "^4.3.6"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
38
|
+
"@modelcontextprotocol/inspector": "^0.21.1",
|
|
37
39
|
"@types/node": "^24.3.0",
|
|
38
40
|
"dotenv-cli": "^10.0.0",
|
|
39
41
|
"openai": "^6.16.0",
|
|
@@ -41,9 +43,11 @@
|
|
|
41
43
|
},
|
|
42
44
|
"scripts": {
|
|
43
45
|
"build": "tsc",
|
|
44
|
-
"inspector": "pnpm build &&
|
|
45
|
-
"test:list": "pnpm build && dotenv -e .env.test -- node ./
|
|
46
|
-
"test:publish": "pnpm build && dotenv -e .env.test -- node ./
|
|
46
|
+
"inspector": "pnpm build && node ./run-inspector.js",
|
|
47
|
+
"test:list": "pnpm build && dotenv -e .env.test -- node ./tests/list.js",
|
|
48
|
+
"test:publish": "pnpm build && dotenv -e .env.test -- node ./tests/publish.js",
|
|
49
|
+
"test:register": "pnpm build && dotenv -e .env.test -- node ./tests/registerTheme.js",
|
|
50
|
+
"test:remove": "pnpm build && dotenv -e .env.test -- node ./tests/removeTheme.js",
|
|
47
51
|
"upgrade:core": "pnpm update @wenyan-md/core"
|
|
48
52
|
}
|
|
49
53
|
}
|