@wenyan-md/mcp 1.0.9 → 1.0.11

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 CHANGED
@@ -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
  文颜目前提供多种形态,覆盖不同使用场景:
@@ -33,12 +31,8 @@
33
31
  - 👉 [MCP 版本](https://github.com/caol64/wenyan-mcp) - 本项目
34
32
  - [核心库](https://github.com/caol64/wenyan-core) - 嵌入 Node / Web 项目
35
33
 
36
- ## 功能特性
34
+ 本仓库是 **文颜的 MCP Server 版本**,基于模型上下文协议(Model Context Protocol),旨在让 AI 助手(如 Claude Desktop)具备自动排版和发布公众号文章的能力。
37
35
 
38
- - 列出并选择支持的文章主题
39
- - 使用内置主题对 Markdown 内容排版
40
- - 自动处理并上传图片(本地 / 网络)
41
- - 一键发布文章到微信公众号草稿箱
42
36
  - **与 AI 深度集成**:[让 AI 帮你管理公众号的排版和发布](https://babyno.top/posts/2025/06/let-ai-help-you-manage-your-gzh-layout-and-publishing/)
43
37
 
44
38
  <video src="https://github.com/user-attachments/assets/2c355f76-f313-48a7-9c31-f0f69e5ec207"></video>
@@ -47,20 +41,6 @@
47
41
  >
48
42
  > 如果与 AI 集成遇到问题,可以参考 [test/list.js](./test/list.js) 和 [test/publish.js](./test/publish.js) 中的完整调用示例。
49
43
 
50
- ## 主题效果预览
51
-
52
- 👉 [内置主题预览](https://yuzhi.tech/docs/wenyan/theme)
53
-
54
- 文颜内置并适配了多个优秀的 Typora 主题,在此感谢原作者:
55
-
56
- - [Orange Heart](https://github.com/evgo2017/typora-theme-orange-heart)
57
- - [Rainbow](https://github.com/thezbm/typora-theme-rainbow)
58
- - [Lapis](https://github.com/YiNNx/typora-theme-lapis)
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)
63
-
64
44
  ## 安装与集成
65
45
 
66
46
  文颜 MCP Server 支持多种运行方式,请根据你的环境选择。
@@ -131,6 +111,81 @@ docker pull caol64/wenyan-mcp
131
111
  > * **环境变量 (`HOST_FILE_PATH`)**:必须与宿主机挂载的文件/图片目录路径保持一致。
132
112
  > * **原理**:你的 Markdown 文件/文章内所引用的本地图片应放置在该目录中,Docker 会自动将其映射,使容器能够读取并上传。
133
113
 
114
+ ## 基本用法
115
+
116
+ ### 列出主题
117
+
118
+ 对AI说
119
+
120
+ > 目前你可以使用哪些公众号主题?
121
+
122
+ AI回复:
123
+
124
+ ```
125
+ 目前我支持的公众号主题包括以下这些,每个主题都有独特的风格和配色,适用于不同的内容调性:
126
+
127
+ 1. **Default(默认)**
128
+ 简洁经典的排版,适合长文阅读,通用性强。
129
+
130
+ 2. **OrangeHeart(橙心)**
131
+ 暖橙色调,优雅而富有活力,适合情感、生活类内容。
132
+ ...
133
+ ```
134
+
135
+ ### 注册主题
136
+
137
+ 对AI说
138
+
139
+ > 帮我把这个css(https://wenyan.yuzhi.tech/manhua.css)注册为一个新的公众号主题,名称为:xiuluochang
140
+
141
+ AI回复:
142
+
143
+ ```
144
+ 好的,已成功将 CSS 样式表注册为新的公众号主题,主题名称为:**xiuluochang**。
145
+
146
+ 您现在可以在公众号后台选择该主题进行应用,样式将按照 `https://wenyan.yuzhi.tech/manhua.css` 的定义进行渲染。
147
+
148
+ 如有需要进一步自定义或更换主题,随时告诉我!
149
+ ```
150
+
151
+ ### 删除主题
152
+
153
+ 对AI说
154
+
155
+ > 把名为“xiuluochang”的公众号主题删除
156
+
157
+ AI回复:
158
+
159
+ ```
160
+ 公众号主题 “xiuluochang” 已成功删除。
161
+ ```
162
+
163
+ ### 发布文章
164
+
165
+ 对AI说
166
+
167
+ > 使用phycat主题将这篇文章发布到微信公众号:`./tests/publish.md`
168
+
169
+ AI回复:
170
+
171
+ ```
172
+ 文章已成功发布至微信公众号草稿箱!🎉
173
+
174
+ - **主题**:phycat
175
+ - **媒体ID**:xxx
176
+
177
+ 您可登录微信公众号后台,在「草稿箱」中查看并编辑文章,确认无误后即可一键发布。如需进一步排版优化、添加封面或设置摘要,我也可以协助您完成!
178
+
179
+ 是否需要我帮您生成一篇发布文案或封面建议? 😊
180
+ ```
181
+
182
+ ## 关于图片自动上传
183
+
184
+ 支持以下图片来源:
185
+
186
+ - 本地路径(如:`/Users/lei/Downloads/result_image.jpg`)
187
+ - 网络路径(如:`https://example.com/image.jpg`)
188
+
134
189
  ## 环境变量说明
135
190
 
136
191
  无论使用哪种方式,都需要配置以下环境变量以连接微信公众号平台:
@@ -157,13 +212,6 @@ cover: /Users/xxx/image.jpg
157
212
  - 如果正文有至少一张图片,可省略,此时将使用其中一张作为封面
158
213
  - 如果正文无图片,则必须提供 cover
159
214
 
160
- ## 关于图片自动上传
161
-
162
- 支持以下图片来源:
163
-
164
- - 本地路径(如:`/Users/lei/Downloads/result_image.jpg`)
165
- - 网络路径(如:`https://example.com/image.jpg`)
166
-
167
215
  ## 微信公众号 IP 白名单
168
216
 
169
217
  > [!IMPORTANT]
package/dist/index.js CHANGED
@@ -10,12 +10,12 @@
10
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
11
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
13
- import { getGzhContent } from "@wenyan-md/core/wrapper";
13
+ import { configStore, renderStyledContent } from "@wenyan-md/core/wrapper";
14
14
  import { publishToDraft } from "@wenyan-md/core/publish";
15
- import { themes } from "@wenyan-md/core/theme";
16
15
  import { getNormalizeFilePath } from "./utils.js";
17
16
  import fs from "node:fs/promises";
18
17
  import path from "node:path";
18
+ import { listThemes, REGISTER_THEME_SCHEMA, registerTheme, REMOVE_THEME_SCHEMA, removeTheme } from "./theme.js";
19
19
  /**
20
20
  * Create an MCP server with capabilities for resources (to list/read notes),
21
21
  * tools (to create new notes), and prompts (to summarize notes).
@@ -57,7 +57,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
57
57
  description: "ID of the theme to use (e.g., default, orangeheart, rainbow, lapis, pie, maize, purple, phycat).",
58
58
  },
59
59
  },
60
- anyOf: [{ required: ["content"] }, { required: ["file"] }],
61
60
  },
62
61
  },
63
62
  {
@@ -68,6 +67,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
68
67
  properties: {},
69
68
  },
70
69
  },
70
+ REGISTER_THEME_SCHEMA,
71
+ REMOVE_THEME_SCHEMA,
71
72
  ],
72
73
  };
73
74
  });
@@ -81,24 +82,37 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
81
82
  // level: "debug",
82
83
  // data: JSON.stringify(request.params.arguments),
83
84
  // });
84
- let content = String(request.params.arguments?.content || "");
85
- const file = String(request.params.arguments?.file || "");
85
+ const contentArg = request.params.arguments?.content;
86
+ const fileArg = request.params.arguments?.file;
87
+ if (!contentArg && !fileArg) {
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 || "");
86
92
  const themeId = String(request.params.arguments?.theme_id || "");
87
- let absoluteDirPath = undefined;
93
+ // 先尝试从已注册的主题中获取主题
94
+ const customTheme = configStore.getThemeById(themeId);
95
+ let absoluteDirPath;
88
96
  if (!content && file) {
89
97
  const normalizePath = getNormalizeFilePath(file);
90
98
  content = await fs.readFile(normalizePath, "utf-8");
99
+ if (!content) {
100
+ throw new Error("Can't read content from the specified file.");
101
+ }
91
102
  absoluteDirPath = path.dirname(normalizePath);
92
103
  }
93
- if (!content) {
94
- throw new Error("Missing content to publish");
95
- }
96
- const gzhContent = await getGzhContent(content, themeId, "solarized-light", true, true);
104
+ const gzhContent = await renderStyledContent(content, {
105
+ themeId,
106
+ hlThemeId: "solarized-light",
107
+ isMacStyle: true,
108
+ isAddFootnote: true,
109
+ themeCss: customTheme,
110
+ });
97
111
  if (!gzhContent.title) {
98
- throw new Error("未能找到文章标题");
112
+ throw new Error("Can't extract a valid title from the frontmatter.");
99
113
  }
100
114
  if (!gzhContent.cover) {
101
- throw new Error("未能找到文章封面");
115
+ throw new Error("Can't extract a valid cover from the frontmatter or article.");
102
116
  }
103
117
  const response = await publishToDraft(gzhContent.title, gzhContent.content, gzhContent.cover, {
104
118
  relativePath: absoluteDirPath,
@@ -113,17 +127,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
113
127
  };
114
128
  }
115
129
  else if (request.params.name === "list_themes") {
116
- const themeResources = Object.entries(themes).map(([id, theme]) => ({
117
- type: "text",
118
- text: JSON.stringify({
119
- id: theme.id,
120
- name: theme.name,
121
- description: theme.description,
122
- }),
123
- }));
124
- return {
125
- content: themeResources,
126
- };
130
+ return listThemes();
131
+ }
132
+ else if (request.params.name === "register_theme") {
133
+ return await registerTheme(String(request.params.arguments?.name || ""), String(request.params.arguments?.path || ""));
134
+ }
135
+ else if (request.params.name === "remove_theme") {
136
+ return removeTheme(String(request.params.arguments?.name || ""));
127
137
  }
128
138
  throw new Error("Unknown tool");
129
139
  });
package/dist/theme.js ADDED
@@ -0,0 +1,112 @@
1
+ import { getAllGzhThemes } from "@wenyan-md/core";
2
+ import { configStore } from "@wenyan-md/core/wrapper";
3
+ import fs from "node:fs/promises";
4
+ import { getNormalizeFilePath } from "./utils.js";
5
+ export const REGISTER_THEME_SCHEMA = {
6
+ name: "register_theme",
7
+ description: "Register a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.",
8
+ inputSchema: {
9
+ type: "object",
10
+ properties: {
11
+ name: {
12
+ type: "string",
13
+ description: "Name of the new custom theme.",
14
+ },
15
+ path: {
16
+ type: "string",
17
+ description: "Path to the new custom theme CSS file. It could be a path to a local file or a URL.",
18
+ },
19
+ },
20
+ },
21
+ };
22
+ export const REMOVE_THEME_SCHEMA = {
23
+ name: "remove_theme",
24
+ description: "Remove a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ name: {
29
+ type: "string",
30
+ description: "Name of the custom theme to remove.",
31
+ },
32
+ },
33
+ },
34
+ };
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/type.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,48 @@
1
+ export declare const REGISTER_THEME_SCHEMA: {
2
+ readonly name: "register_theme";
3
+ readonly description: "Register a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.";
4
+ readonly inputSchema: {
5
+ readonly type: "object";
6
+ readonly properties: {
7
+ readonly name: {
8
+ readonly type: "string";
9
+ readonly description: "Name of the new custom theme.";
10
+ };
11
+ readonly path: {
12
+ readonly type: "string";
13
+ readonly description: "Path to the new custom theme CSS file. It could be a path to a local file or a URL.";
14
+ };
15
+ };
16
+ };
17
+ };
18
+ export declare const REMOVE_THEME_SCHEMA: {
19
+ readonly name: "remove_theme";
20
+ readonly description: "Remove a custom theme compatible with the 'publish_article' tool to publish an article to '微信公众号'.";
21
+ readonly inputSchema: {
22
+ readonly type: "object";
23
+ readonly properties: {
24
+ readonly name: {
25
+ readonly type: "string";
26
+ readonly description: "Name of the custom theme to remove.";
27
+ };
28
+ };
29
+ };
30
+ };
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
+ };
@@ -0,0 +1,8 @@
1
+ export type OutputType = "text";
2
+ export interface OutputObject {
3
+ type: OutputType;
4
+ text: string;
5
+ }
6
+ export interface ResponseObject {
7
+ content: OutputObject[];
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/mcp",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
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,7 +31,10 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@modelcontextprotocol/sdk": "0.6.0",
34
- "@wenyan-md/core": "^1.0.17"
34
+ "@wenyan-md/core": "^2.0.2",
35
+ "form-data-encoder": "^4.1.0",
36
+ "formdata-node": "^6.0.3",
37
+ "jsdom": "^27.4.0"
35
38
  },
36
39
  "devDependencies": {
37
40
  "@types/node": "^24.3.0",
@@ -42,8 +45,10 @@
42
45
  "scripts": {
43
46
  "build": "tsc",
44
47
  "inspector": "pnpm build && dotenv -e .env.test -- node ./run-inspector.js",
45
- "test:list": "pnpm build && dotenv -e .env.test -- node ./test/list.js",
46
- "test:publish": "pnpm build && dotenv -e .env.test -- node ./test/publish.js",
48
+ "test:list": "pnpm build && dotenv -e .env.test -- node ./tests/list.js",
49
+ "test:publish": "pnpm build && dotenv -e .env.test -- node ./tests/publish.js",
50
+ "test:register": "pnpm build && dotenv -e .env.test -- node ./tests/registerTheme.js",
51
+ "test:remove": "pnpm build && dotenv -e .env.test -- node ./tests/removeTheme.js",
47
52
  "upgrade:core": "pnpm update @wenyan-md/core"
48
53
  }
49
54
  }