@wenyan-md/mcp 1.0.11 → 2.0.1
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 +128 -83
- package/dist/index.js +47 -137
- package/dist/mcpServer.js +93 -0
- package/dist/publish.js +48 -0
- package/dist/theme.js +8 -82
- 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 +8 -18
- package/dist/types/utils.d.ts +23 -1
- package/dist/utils.js +47 -23
- package/package.json +6 -4
- /package/dist/types/{type.d.ts → types.d.ts} +0 -0
- /package/dist/{type.js → types.js} +0 -0
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
|
- 知乎
|
|
@@ -29,39 +29,50 @@
|
|
|
29
29
|
- [跨平台桌面版](https://github.com/caol64/wenyan-pc) - Windows/Linux
|
|
30
30
|
- [CLI 版本](https://github.com/caol64/wenyan-cli) - 命令行 / CI 自动化发布
|
|
31
31
|
- 👉 [MCP 版本](https://github.com/caol64/wenyan-mcp) - 本项目
|
|
32
|
-
- [核心库](https://github.com/caol64/wenyan-core) - 嵌入 Node / Web 项目
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
## 文颜 MCP Server 是什么?
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
简单来说,它打通了“AI 写作”与“公众号发文”的通道。
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
基于 MCP 协议,Claude Desktop 等 AI 客户端现在可以直接调用文颜(Wenyan)的排版引擎。写完文章后,不需要再去第三方编辑器里来回复制粘贴,直接让 AI 帮你排版并塞进微信草稿箱。
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
**核心特性:**
|
|
40
|
+
|
|
41
|
+
- **绕过排版工具**:AI 生成的 Markdown 直接转成微信富文本并上传,省去中间步骤。
|
|
42
|
+
- **对话式排版**:直接打字跟 AI 说“换个橙色风格主题”,样式自动生效。
|
|
43
|
+
- **不出窗口完成闭环**:在同一个聊天框里,顺滑搞定“想选题 -> 写文章 -> 调排版 -> 存草稿”的所有操作。
|
|
43
44
|
|
|
44
|
-
|
|
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)
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
## 功能特性
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
- 一键发布 Markdown 到微信公众号草稿箱
|
|
52
|
+
- 自动上传本地图片与封面
|
|
53
|
+
- 支持远程 Server 发布(绕过 IP 白名单限制)
|
|
54
|
+
- 内置多套精美排版主题
|
|
55
|
+
- 支持自定义主题
|
|
56
|
+
- 提供标准 MCP Tool 接口
|
|
57
|
+
- 支持 AI 自动调用:
|
|
58
|
+
- 渲染 Markdown
|
|
59
|
+
- 主题管理
|
|
60
|
+
- 发布草稿
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
## 快速开始
|
|
63
|
+
|
|
64
|
+
**安装**
|
|
51
65
|
|
|
52
66
|
```bash
|
|
53
67
|
npm install -g @wenyan-md/mcp
|
|
54
68
|
```
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
在你的 MCP 配置文件中加入以下内容:
|
|
70
|
+
**Claude Desktop 配置 (claude_desktop_config.json):**:
|
|
59
71
|
|
|
60
72
|
```json
|
|
61
73
|
{
|
|
62
74
|
"mcpServers": {
|
|
63
75
|
"wenyan-mcp": {
|
|
64
|
-
"name": "公众号助手",
|
|
65
76
|
"command": "wenyan-mcp",
|
|
66
77
|
"env": {
|
|
67
78
|
"WECHAT_APP_ID": "your_app_id",
|
|
@@ -72,45 +83,6 @@ npm install -g @wenyan-md/mcp
|
|
|
72
83
|
}
|
|
73
84
|
```
|
|
74
85
|
|
|
75
|
-
### 方式二:Docker 运行(推荐)
|
|
76
|
-
|
|
77
|
-
适合部署到服务器环境,或希望环境隔离的用户。
|
|
78
|
-
|
|
79
|
-
**拉取镜像:**
|
|
80
|
-
|
|
81
|
-
```bash
|
|
82
|
-
docker pull caol64/wenyan-mcp
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
**配置 MCP Client:**
|
|
86
|
-
|
|
87
|
-
```json
|
|
88
|
-
{
|
|
89
|
-
"mcpServers": {
|
|
90
|
-
"wenyan-mcp": {
|
|
91
|
-
"name": "公众号助手",
|
|
92
|
-
"command": "docker",
|
|
93
|
-
"args": [
|
|
94
|
-
"run",
|
|
95
|
-
"--rm",
|
|
96
|
-
"-i",
|
|
97
|
-
"-v", "/your/host/file/path:/mnt/host-downloads",
|
|
98
|
-
"-e", "WECHAT_APP_ID=your_app_id",
|
|
99
|
-
"-e", "WECHAT_APP_SECRET=your_app_secret",
|
|
100
|
-
"-e", "HOST_FILE_PATH=/your/host/file/path",
|
|
101
|
-
"caol64/wenyan-mcp"
|
|
102
|
-
]
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
> **Docker 配置特别说明:**
|
|
109
|
-
>
|
|
110
|
-
> * **挂载目录 (`-v`)**:必须将宿主机的文件/图片目录挂载到容器内的 `/mnt/host-downloads`。
|
|
111
|
-
> * **环境变量 (`HOST_FILE_PATH`)**:必须与宿主机挂载的文件/图片目录路径保持一致。
|
|
112
|
-
> * **原理**:你的 Markdown 文件/文章内所引用的本地图片应放置在该目录中,Docker 会自动将其映射,使容器能够读取并上传。
|
|
113
|
-
|
|
114
86
|
## 基本用法
|
|
115
87
|
|
|
116
88
|
### 列出主题
|
|
@@ -179,28 +151,35 @@ AI回复:
|
|
|
179
151
|
是否需要我帮您生成一篇发布文案或封面建议? 😊
|
|
180
152
|
```
|
|
181
153
|
|
|
182
|
-
##
|
|
154
|
+
## 概念
|
|
183
155
|
|
|
184
|
-
|
|
156
|
+
### 环境变量配置
|
|
185
157
|
|
|
186
|
-
|
|
187
|
-
|
|
158
|
+
> [!IMPORTANT]
|
|
159
|
+
>
|
|
160
|
+
> 请确保 MCP 启动时已配置如下环境变量,否则上传接口将调用失败。
|
|
188
161
|
|
|
189
|
-
|
|
162
|
+
- `WECHAT_APP_ID`
|
|
163
|
+
- `WECHAT_APP_SECRET`
|
|
190
164
|
|
|
191
|
-
|
|
165
|
+
### 微信公众号 IP 白名单
|
|
192
166
|
|
|
193
|
-
|
|
194
|
-
|
|
167
|
+
> [!IMPORTANT]
|
|
168
|
+
>
|
|
169
|
+
> 请确保运行文颜的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
|
|
170
|
+
|
|
171
|
+
配置说明文档:[https://yuzhi.tech/docs/wenyan/upload](https://yuzhi.tech/docs/wenyan/upload)
|
|
195
172
|
|
|
196
|
-
|
|
173
|
+
### 文章格式
|
|
197
174
|
|
|
198
|
-
为了正确上传文章,每篇 Markdown
|
|
175
|
+
为了正确上传文章,每篇 Markdown 顶部需要包含一段 `frontmatter`:
|
|
199
176
|
|
|
200
177
|
```md
|
|
201
178
|
---
|
|
202
179
|
title: 在本地跑一个大语言模型(2) - 给模型提供外部知识库
|
|
203
180
|
cover: /Users/xxx/image.jpg
|
|
181
|
+
author: xxx
|
|
182
|
+
source_url: http://
|
|
204
183
|
---
|
|
205
184
|
```
|
|
206
185
|
|
|
@@ -209,34 +188,100 @@ cover: /Users/xxx/image.jpg
|
|
|
209
188
|
- `title` 文章标题(必填)
|
|
210
189
|
- `cover` 文章封面
|
|
211
190
|
- 本地路径或网络图片
|
|
212
|
-
-
|
|
213
|
-
|
|
191
|
+
- 如果正文中已有图片,可省略
|
|
192
|
+
- `author` 文章作者
|
|
193
|
+
- `source_url` 原文地址
|
|
214
194
|
|
|
215
|
-
|
|
195
|
+
**[示例文章](tests/publish.md)**
|
|
216
196
|
|
|
217
|
-
|
|
218
|
-
>
|
|
219
|
-
> 请确保运行文颜 MCP Server 的机器 IP 已加入微信公众号后台的 IP 白名单,否则上传接口将调用失败。
|
|
197
|
+
### 文内图片和文章封面
|
|
220
198
|
|
|
221
|
-
|
|
199
|
+
把文章发布到公众号之前,文颜会按照微信要求自动处理文章内的所有图片,将其上传到公众号素材库。目前文颜对于以下图片都能很好的支持:
|
|
222
200
|
|
|
223
|
-
|
|
201
|
+
- 本地硬盘绝对路径(如:`/Users/xxx/image.jpg`)
|
|
202
|
+
- 网络路径(如:`https://example.com/image.jpg`)
|
|
203
|
+
- 当前文章的相对路径(如:`./assets/image.png`)
|
|
224
204
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
cover: /Users/lei/Downloads/result_image.jpg
|
|
229
|
-
---
|
|
205
|
+
## Server 模式
|
|
206
|
+
|
|
207
|
+
相较于纯本地运行的**本地模式(Stdio Mode)**,`wenyan-mcp`还提供了 **远程客户端模式(Client–Server Mode)**。两种模式运行效果完全一致,你可以根据运行环境和网络条件选择最合适的方式。
|
|
230
208
|
|
|
231
|
-
|
|
209
|
+
在本地模式下,MCP 直接调用微信公众号 API 完成图片上传和草稿发布。
|
|
232
210
|
|
|
233
|
-
|
|
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 部署
|
|
234
250
|
|
|
235
|
-
|
|
251
|
+
适合不希望安装 Node.js 环境的用户。
|
|
236
252
|
|
|
237
|
-
|
|
253
|
+
```bash
|
|
254
|
+
docker pull caol64/wenyan-mcp:latest
|
|
238
255
|
```
|
|
239
256
|
|
|
257
|
+
* **Claude Desktop 配置:**:
|
|
258
|
+
|
|
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
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
> **Docker 配置特别说明:**
|
|
280
|
+
>
|
|
281
|
+
> * **挂载目录 (`-v`)**:必须将宿主机的文件/图片目录挂载到容器内的 `/mnt/host-downloads`。
|
|
282
|
+
> * **环境变量 (`HOST_FILE_PATH`)**:必须与宿主机挂载的文件/图片目录路径保持一致。
|
|
283
|
+
> * **原理**:你的 Markdown 文件/文章内所引用的本地图片应放置在该目录中,Docker 会自动将其映射,使容器能够读取并上传。
|
|
284
|
+
|
|
240
285
|
## 如何调试
|
|
241
286
|
|
|
242
287
|
推荐使用官方 Inspector 进行调试:
|
package/dist/index.js
CHANGED
|
@@ -1,151 +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 { getNormalizeFilePath } from "./utils.js";
|
|
16
|
-
import fs from "node:fs/promises";
|
|
17
|
-
import path from "node:path";
|
|
18
|
-
import { listThemes, REGISTER_THEME_SCHEMA, registerTheme, REMOVE_THEME_SCHEMA, removeTheme } from "./theme.js";
|
|
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
|
-
REGISTER_THEME_SCHEMA,
|
|
71
|
-
REMOVE_THEME_SCHEMA,
|
|
72
|
-
],
|
|
73
|
-
};
|
|
74
|
-
});
|
|
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
|
+
}
|
|
75
14
|
/**
|
|
76
|
-
*
|
|
77
|
-
* 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.
|
|
78
16
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
throw new Error("You must provide either 'content' or 'file' to publish an article.");
|
|
89
|
-
}
|
|
90
|
-
let content = String(contentArg || "");
|
|
91
|
-
const file = String(fileArg || "");
|
|
92
|
-
const themeId = String(request.params.arguments?.theme_id || "");
|
|
93
|
-
// 先尝试从已注册的主题中获取主题
|
|
94
|
-
const customTheme = configStore.getThemeById(themeId);
|
|
95
|
-
let absoluteDirPath;
|
|
96
|
-
if (!content && file) {
|
|
97
|
-
const normalizePath = getNormalizeFilePath(file);
|
|
98
|
-
content = await fs.readFile(normalizePath, "utf-8");
|
|
99
|
-
if (!content) {
|
|
100
|
-
throw new Error("Can't read content from the specified file.");
|
|
101
|
-
}
|
|
102
|
-
absoluteDirPath = path.dirname(normalizePath);
|
|
103
|
-
}
|
|
104
|
-
const gzhContent = await renderStyledContent(content, {
|
|
105
|
-
themeId,
|
|
106
|
-
hlThemeId: "solarized-light",
|
|
107
|
-
isMacStyle: true,
|
|
108
|
-
isAddFootnote: true,
|
|
109
|
-
themeCss: customTheme,
|
|
110
|
-
});
|
|
111
|
-
if (!gzhContent.title) {
|
|
112
|
-
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];
|
|
113
26
|
}
|
|
114
|
-
|
|
115
|
-
|
|
27
|
+
const apiKeyIndex = args.indexOf("--api-key");
|
|
28
|
+
if (apiKeyIndex !== -1 && args[apiKeyIndex + 1] && !args[apiKeyIndex + 1].startsWith("--")) {
|
|
29
|
+
globalStates.apiKey = args[apiKeyIndex + 1];
|
|
116
30
|
}
|
|
117
|
-
const response = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover, {
|
|
118
|
-
relativePath: absoluteDirPath,
|
|
119
|
-
});
|
|
120
|
-
return {
|
|
121
|
-
content: [
|
|
122
|
-
{
|
|
123
|
-
type: "text",
|
|
124
|
-
text: `Your article was successfully published to '公众号草稿箱'. The media ID is ${response.media_id}.`,
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
else if (request.params.name === "list_themes") {
|
|
130
|
-
return listThemes();
|
|
131
31
|
}
|
|
132
|
-
else
|
|
133
|
-
|
|
32
|
+
else {
|
|
33
|
+
console.error("[Init] Starting Wenyan MCP server in local mode...");
|
|
134
34
|
}
|
|
135
|
-
|
|
136
|
-
return removeTheme(String(request.params.arguments?.name || ""));
|
|
137
|
-
}
|
|
138
|
-
throw new Error("Unknown tool");
|
|
139
|
-
});
|
|
140
|
-
/**
|
|
141
|
-
* Start the server using stdio transport.
|
|
142
|
-
* This allows the server to communicate via standard input/output streams.
|
|
143
|
-
*/
|
|
144
|
-
async function main() {
|
|
145
|
-
const transport = new StdioServerTransport();
|
|
146
|
-
await server.connect(transport);
|
|
35
|
+
await mainStdio();
|
|
147
36
|
}
|
|
37
|
+
// ==========================================
|
|
38
|
+
// 全局异常与信号处理 (Graceful Shutdown)
|
|
39
|
+
// ==========================================
|
|
148
40
|
main().catch((error) => {
|
|
149
|
-
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);
|
|
150
51
|
process.exit(1);
|
|
151
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
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
};
|
|
5
9
|
export const REGISTER_THEME_SCHEMA = {
|
|
6
10
|
name: "register_theme",
|
|
7
11
|
description: "Register a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.",
|
|
@@ -32,81 +36,3 @@ export const REMOVE_THEME_SCHEMA = {
|
|
|
32
36
|
},
|
|
33
37
|
},
|
|
34
38
|
};
|
|
35
|
-
export function listThemes() {
|
|
36
|
-
const themes = getAllGzhThemes();
|
|
37
|
-
const customThemes = configStore.getThemes();
|
|
38
|
-
return {
|
|
39
|
-
content: [
|
|
40
|
-
...themes.map((theme) => ({
|
|
41
|
-
type: "text",
|
|
42
|
-
text: JSON.stringify({
|
|
43
|
-
id: theme.meta.id,
|
|
44
|
-
name: theme.meta.name,
|
|
45
|
-
description: theme.meta.description,
|
|
46
|
-
}),
|
|
47
|
-
})),
|
|
48
|
-
...customThemes.map((theme) => ({
|
|
49
|
-
type: "text",
|
|
50
|
-
text: JSON.stringify({
|
|
51
|
-
id: theme.id,
|
|
52
|
-
name: theme.name ?? theme.id,
|
|
53
|
-
description: theme.description ?? "自定义主题,暂无描述。",
|
|
54
|
-
}),
|
|
55
|
-
})),
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
export async function registerTheme(name, path) {
|
|
60
|
-
if (!name || !path) {
|
|
61
|
-
throw new Error("When adding a theme, you must provide a name and a path.");
|
|
62
|
-
}
|
|
63
|
-
if (checkThemeExists(name) || checkCustomThemeExists(name)) {
|
|
64
|
-
throw new Error("A theme with the given name already exists.");
|
|
65
|
-
}
|
|
66
|
-
if (path.startsWith("http")) {
|
|
67
|
-
const response = await fetch(path);
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
throw new Error(`Failed to retrieve theme from url: ${response.statusText}`);
|
|
70
|
-
}
|
|
71
|
-
const content = await response.text();
|
|
72
|
-
configStore.addThemeToConfig(name, content);
|
|
73
|
-
}
|
|
74
|
-
else {
|
|
75
|
-
const normalizePath = getNormalizeFilePath(path);
|
|
76
|
-
const content = await fs.readFile(normalizePath, "utf-8");
|
|
77
|
-
configStore.addThemeToConfig(name, content);
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
content: [
|
|
81
|
-
{
|
|
82
|
-
type: "text",
|
|
83
|
-
text: `Theme "${name}" has been added successfully.`,
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
export function removeTheme(name) {
|
|
89
|
-
if (checkThemeExists(name)) {
|
|
90
|
-
throw new Error(`Can't remove builtin theme "${name}"`);
|
|
91
|
-
}
|
|
92
|
-
if (!checkCustomThemeExists(name)) {
|
|
93
|
-
throw new Error(`Custom theme "${name}" does not exist`);
|
|
94
|
-
}
|
|
95
|
-
configStore.deleteThemeFromConfig(name);
|
|
96
|
-
return {
|
|
97
|
-
content: [
|
|
98
|
-
{
|
|
99
|
-
type: "text",
|
|
100
|
-
text: `Theme "${name}" has been removed successfully.`,
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
function checkThemeExists(themeId) {
|
|
106
|
-
const themes = getAllGzhThemes();
|
|
107
|
-
return themes.some((theme) => theme.meta.id === themeId);
|
|
108
|
-
}
|
|
109
|
-
function checkCustomThemeExists(themeId) {
|
|
110
|
-
const customThemes = configStore.getThemes();
|
|
111
|
-
return customThemes.some((theme) => theme.id === themeId);
|
|
112
|
-
}
|
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
|
+
}>;
|
package/dist/types/theme.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
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
|
+
};
|
|
1
9
|
export declare const REGISTER_THEME_SCHEMA: {
|
|
2
10
|
readonly name: "register_theme";
|
|
3
11
|
readonly description: "Register a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.";
|
|
@@ -28,21 +36,3 @@ export declare const REMOVE_THEME_SCHEMA: {
|
|
|
28
36
|
};
|
|
29
37
|
};
|
|
30
38
|
};
|
|
31
|
-
export declare function listThemes(): {
|
|
32
|
-
content: {
|
|
33
|
-
type: "text";
|
|
34
|
-
text: string;
|
|
35
|
-
}[];
|
|
36
|
-
};
|
|
37
|
-
export declare function registerTheme(name: string, path: string): Promise<{
|
|
38
|
-
content: {
|
|
39
|
-
type: string;
|
|
40
|
-
text: string;
|
|
41
|
-
}[];
|
|
42
|
-
}>;
|
|
43
|
-
export declare function removeTheme(name: string): {
|
|
44
|
-
content: {
|
|
45
|
-
type: string;
|
|
46
|
-
text: string;
|
|
47
|
-
}[];
|
|
48
|
-
};
|
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/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.1",
|
|
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,12 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "0.6.0",
|
|
34
|
-
"@wenyan-md/core": "^2.0.
|
|
34
|
+
"@wenyan-md/core": "^2.0.8",
|
|
35
35
|
"form-data-encoder": "^4.1.0",
|
|
36
36
|
"formdata-node": "^6.0.3",
|
|
37
|
-
"jsdom": "^27.4.0"
|
|
37
|
+
"jsdom": "^27.4.0",
|
|
38
|
+
"zod": "^4.3.6"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
41
|
+
"@modelcontextprotocol/inspector": "^0.21.1",
|
|
40
42
|
"@types/node": "^24.3.0",
|
|
41
43
|
"dotenv-cli": "^10.0.0",
|
|
42
44
|
"openai": "^6.16.0",
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
},
|
|
45
47
|
"scripts": {
|
|
46
48
|
"build": "tsc",
|
|
47
|
-
"inspector": "pnpm build &&
|
|
49
|
+
"inspector": "pnpm build && node ./run-inspector.js",
|
|
48
50
|
"test:list": "pnpm build && dotenv -e .env.test -- node ./tests/list.js",
|
|
49
51
|
"test:publish": "pnpm build && dotenv -e .env.test -- node ./tests/publish.js",
|
|
50
52
|
"test:register": "pnpm build && dotenv -e .env.test -- node ./tests/registerTheme.js",
|
|
File without changes
|
|
File without changes
|