word-generator-mcp 1.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.
Files changed (3) hide show
  1. package/README.md +158 -0
  2. package/package.json +21 -0
  3. package/server.js +249 -0
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # word-generator-mcp
2
+
3
+ Word文档生成 MCP Server,实现以下目标:
4
+
5
+ 1. 读取并理解skills的规范
6
+ 2. 根据规范生成对应的Node.js脚本(不保存到本地)
7
+ 3. 执行Node.js脚本,生成对应的Word文档
8
+
9
+ ## 功能
10
+
11
+ ### 工具列表
12
+
13
+ | 工具名称 | 功能描述 |
14
+ |---------|---------|
15
+ | `list_skills` | 列出所有可用的skills(读取.kilocode/skills目录) |
16
+ | `read_skill` | 读取指定skill的SKILL.md文件内容 |
17
+ | `generate_word_script` | 根据skill规范生成Node.js Word生成脚本(返回脚本内容,不保存) |
18
+ | `generate_word_document` | 根据skill规范和报告数据直接生成Word文档 |
19
+
20
+ ### Word格式规范
21
+
22
+ 根据 `.kilocode/skills/word-format-common/SKILL.md` 规范:
23
+
24
+ - **标题颜色**:全部黑色(RGB 0,0,0)
25
+ - **标题字体**:全部黑体(Heiti/SimHei)
26
+ - **主标题**:居中,黑体,22pt
27
+ - **一级标题**:黑体,18pt
28
+ - **二/三级标题**:黑体,16pt
29
+ - **正文字体**:仿宋
30
+ - **首行缩进**:2字符
31
+ - **观点标红**:框架建议与观点性内容使用红色加粗
32
+
33
+ ## 安装
34
+
35
+ ```bash
36
+ cd scripts/word-generator-mcp
37
+ npm install
38
+ ```
39
+
40
+ ## 在MCP客户端中配置
41
+
42
+ ### 方式一:npx(推荐)
43
+
44
+ ```json
45
+ {
46
+ "word-generator": {
47
+ "command": "npx",
48
+ "args": ["-y", "file:./scripts/word-generator-mcp"],
49
+ "alwaysAllow": ["*"]
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### 方式二:本地安装后直接运行
55
+
56
+ ```json
57
+ {
58
+ "word-generator": {
59
+ "command": "node",
60
+ "args": ["scripts/word-generator-mcp/server.js"],
61
+ "alwaysAllow": ["*"]
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## 使用示例
67
+
68
+ ### 1. 列出所有skills
69
+
70
+ ```json
71
+ {
72
+ "name": "list_skills",
73
+ "arguments": {}
74
+ }
75
+ ```
76
+
77
+ ### 2. 读取指定skill
78
+
79
+ ```json
80
+ {
81
+ "name": "read_skill",
82
+ "arguments": {
83
+ "skill_name": "word-format-common"
84
+ }
85
+ }
86
+ ```
87
+
88
+ ### 3. 生成Word文档
89
+
90
+ ```json
91
+ {
92
+ "name": "generate_word_document",
93
+ "arguments": {
94
+ "skill_name": "sentiment-writer",
95
+ "report_data": {
96
+ "title": "舆情分析报告",
97
+ "opening": "本报告旨在分析...",
98
+ "sections": [
99
+ {
100
+ "heading": "一、事件概述",
101
+ "subsections": [
102
+ {
103
+ "heading": "(一)背景",
104
+ "paragraphs": [
105
+ { "text": "这是正文内容..." },
106
+ { "text": "这是观点性内容(红色加粗)", "isViewPoint": true }
107
+ ]
108
+ }
109
+ ]
110
+ }
111
+ ],
112
+ "references": [
113
+ "【1】来源链接..."
114
+ ]
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ ## 报告数据结构
121
+
122
+ ```typescript
123
+ interface ReportData {
124
+ title: string; // 报告标题
125
+ opening?: string; // 开篇段落
126
+ sections: Section[]; // 各节内容
127
+ references?: string[]; // 参考文献
128
+ }
129
+
130
+ interface Section {
131
+ heading: string; // 一级标题(如:一、事件概述)
132
+ subsections?: Subsection[]; // 二级内容
133
+ paragraphs?: Paragraph[]; // 直接段落
134
+ }
135
+
136
+ interface Subsection {
137
+ heading: string; // 二级标题(如:(一)背景)
138
+ paragraphs: Paragraph[]; // 段落列表
139
+ }
140
+
141
+ interface Paragraph {
142
+ text: string; // 段落文本
143
+ isViewPoint?: boolean; // 是否为观点性内容(红色加粗)
144
+ }
145
+ ```
146
+
147
+ ## 输出
148
+
149
+ - 生成的Word文档保存在 `word/` 目录
150
+ - 文件名格式:`{标题}_{时间戳}.docx`
151
+
152
+ ## 要求
153
+
154
+ - Node.js >= 18
155
+
156
+ ## License
157
+
158
+ MIT
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "word-generator-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server:根据 report_data 生成 Word 报告/简报,保存到当前工作目录(项目根)",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "bin": {
8
+ "word-generator-mcp": "server.js"
9
+ },
10
+ "scripts": { "start": "node server.js" },
11
+ "keywords": ["mcp", "word", "docx", "report", "briefing", "model-context-protocol"],
12
+ "license": "MIT",
13
+ "engines": { "node": ">=18" },
14
+ "files": ["server.js"],
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.0.0",
17
+ "ajv": "^8.17.1",
18
+ "ajv-formats": "^3.0.1",
19
+ "docx": "^8.5.0"
20
+ }
21
+ }
package/server.js ADDED
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Word Generator MCP Server
4
+ * 功能:根据 report_data 一键生成 Word 报告/简报,保存到 word/ 目录。
5
+ */
6
+
7
+ import { Server } from "@modelcontextprotocol/sdk/server";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import {
10
+ ListToolsRequestSchema,
11
+ CallToolRequestSchema,
12
+ } from "@modelcontextprotocol/sdk/types.js";
13
+ import { Document, Packer, Paragraph, TextRun, AlignmentType, HeadingLevel } from "docx";
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+ import { fileURLToPath } from "url";
17
+
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
+
21
+ // 保存目录:npx 或本地运行时均使用当前工作目录(用户项目根),便于其他电脑直接 npx 调用
22
+ const WORD_DIR = process.cwd();
23
+
24
+ // 所有标题字体颜色均为黑色;正文黑色,仅观点性内容(isViewPoint)为红色;标题一居中,其余标题居左
25
+ const WORD_CONFIG = {
26
+ title: { font: "黑体", size: 44, color: "000000", bold: true },
27
+ heading1: { font: "黑体", size: 36, color: "000000", bold: true },
28
+ heading2: { font: "黑体", size: 32, color: "000000", bold: true },
29
+ body: { font: "仿宋", size: 28, color: "000000" },
30
+ viewPoint: { font: "仿宋", size: 28, color: "C00000", bold: true }
31
+ };
32
+
33
+ function createTextRun(text, style) {
34
+ return new TextRun({
35
+ text: text || "",
36
+ font: style.font,
37
+ size: style.size,
38
+ bold: style.bold || false,
39
+ color: style.color
40
+ });
41
+ }
42
+
43
+ // 数字转中文大写(一、二、三、四、五…),用于「数据来源与参考文献」等标题编号
44
+ const CHINESE_NUMBERS = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
45
+ function toChineseNumber(n) {
46
+ if (n <= 0 || n > 99) return String(n);
47
+ if (n <= 10) return CHINESE_NUMBERS[n];
48
+ if (n < 20) return "十" + CHINESE_NUMBERS[n - 10];
49
+ if (n < 100) {
50
+ const tens = Math.floor(n / 10);
51
+ const ones = n % 10;
52
+ return CHINESE_NUMBERS[tens] + "十" + (ones > 0 ? CHINESE_NUMBERS[ones] : "");
53
+ }
54
+ return String(n);
55
+ }
56
+
57
+ async function generateWordDocument(reportData) {
58
+ const children = [];
59
+
60
+ if (reportData.title) {
61
+ children.push(new Paragraph({
62
+ alignment: AlignmentType.CENTER,
63
+ heading: HeadingLevel.TITLE,
64
+ children: [createTextRun(reportData.title, WORD_CONFIG.title)]
65
+ }));
66
+ children.push(new Paragraph({ children: [] }));
67
+ }
68
+
69
+ if (reportData.opening) {
70
+ children.push(new Paragraph({
71
+ alignment: AlignmentType.START,
72
+ indent: { firstLine: 560 },
73
+ children: [createTextRun(reportData.opening, WORD_CONFIG.body)]
74
+ }));
75
+ children.push(new Paragraph({ children: [] }));
76
+ }
77
+
78
+ if (reportData.sections && Array.isArray(reportData.sections)) {
79
+ for (const section of reportData.sections) {
80
+ if (section.heading) {
81
+ children.push(new Paragraph({
82
+ alignment: AlignmentType.START,
83
+ heading: HeadingLevel.HEADING_1,
84
+ children: [createTextRun(section.heading, WORD_CONFIG.heading1)]
85
+ }));
86
+ }
87
+
88
+ if (section.subsections && Array.isArray(section.subsections)) {
89
+ for (const sub of section.subsections) {
90
+ if (sub.heading) {
91
+ children.push(new Paragraph({
92
+ alignment: AlignmentType.START,
93
+ heading: HeadingLevel.HEADING_2,
94
+ children: [createTextRun(sub.heading, WORD_CONFIG.heading2)]
95
+ }));
96
+ }
97
+ if (sub.paragraphs && Array.isArray(sub.paragraphs)) {
98
+ for (const para of sub.paragraphs) {
99
+ const text = typeof para === 'string' ? para : (para.text || '');
100
+ const isViewPoint = typeof para === 'object' && para.isViewPoint;
101
+ const style = isViewPoint ? WORD_CONFIG.viewPoint : WORD_CONFIG.body;
102
+ children.push(new Paragraph({
103
+ alignment: AlignmentType.START,
104
+ indent: { firstLine: 560 },
105
+ children: [createTextRun(text, style)]
106
+ }));
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ if (section.paragraphs && Array.isArray(section.paragraphs)) {
113
+ for (const para of section.paragraphs) {
114
+ const text = typeof para === 'string' ? para : (para.text || '');
115
+ const isViewPoint = typeof para === 'object' && para.isViewPoint;
116
+ const style = isViewPoint ? WORD_CONFIG.viewPoint : WORD_CONFIG.body;
117
+ children.push(new Paragraph({
118
+ alignment: AlignmentType.START,
119
+ indent: { firstLine: 560 },
120
+ children: [createTextRun(text, style)]
121
+ }));
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ if (reportData.references && Array.isArray(reportData.references) && reportData.references.length > 0) {
128
+ const sectionCount = reportData.sections ? reportData.sections.length : 0;
129
+ const refHeadingNum = sectionCount + 1;
130
+ const refHeadingText = `${toChineseNumber(refHeadingNum)}、数据来源与参考文献`;
131
+ children.push(new Paragraph({
132
+ alignment: AlignmentType.START,
133
+ heading: HeadingLevel.HEADING_1,
134
+ children: [createTextRun(refHeadingText, WORD_CONFIG.heading1)]
135
+ }));
136
+ for (const ref of reportData.references) {
137
+ const refText = typeof ref === 'string' ? ref : String(ref);
138
+ children.push(new Paragraph({
139
+ alignment: AlignmentType.START,
140
+ indent: { firstLine: 560 },
141
+ children: [createTextRun(refText, { ...WORD_CONFIG.body, size: 24 })]
142
+ }));
143
+ }
144
+ }
145
+
146
+ const doc = new Document({
147
+ numbering: {
148
+ config: [{
149
+ reference: "default-numbering",
150
+ levels: [{ level: 0, format: "decimal", text: "%1.", alignment: AlignmentType.START }]
151
+ }]
152
+ },
153
+ sections: [{
154
+ properties: {
155
+ page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } }
156
+ },
157
+ children: children
158
+ }]
159
+ });
160
+
161
+ const buffer = await Packer.toBuffer(doc);
162
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
163
+ const safeTitle = (reportData.title || "报告").replace(/[\\/:*?"<>|]/g, "_");
164
+ const filename = `${safeTitle}_${timestamp}.docx`;
165
+ const filepath = path.join(WORD_DIR, filename);
166
+ fs.writeFileSync(filepath, buffer);
167
+ return { success: true, filepath: filepath, size: buffer.length };
168
+ }
169
+
170
+ const server = new Server(
171
+ { name: "word-generator-mcp", version: "2.0.0" },
172
+ { capabilities: { tools: {} } }
173
+ );
174
+
175
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
176
+ return {
177
+ tools: [{
178
+ name: "generate_word_document",
179
+ description: "根据报告数据直接生成Word文档(保存到项目根目录)。参数:skill_name(可选), report_data(必需,包含title、opening、sections、references)",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {
183
+ skill_name: { type: "string", description: "skill名称(可选,如 sentiment-writer / briefing-writer)" },
184
+ report_data: {
185
+ type: "object",
186
+ description: "报告数据:title、opening、sections(每节含heading、subsections或paragraphs;paragraphs项可含 isViewPoint: true 表示红加粗)、references(字符串数组)"
187
+ }
188
+ },
189
+ required: ["report_data"]
190
+ }
191
+ }]
192
+ };
193
+ });
194
+
195
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
196
+ try {
197
+ const toolName = request.params.name;
198
+ const args = request.params.arguments || {};
199
+ if (toolName === "generate_word_document") {
200
+ const reportData = args.report_data;
201
+ if (!reportData) {
202
+ return {
203
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "缺少report_data参数" }) }],
204
+ isError: true
205
+ };
206
+ }
207
+ try {
208
+ const result = await generateWordDocument(reportData);
209
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
210
+ } catch (err) {
211
+ return {
212
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: err.message, stack: err.stack }) }],
213
+ isError: true
214
+ };
215
+ }
216
+ }
217
+ return {
218
+ content: [{ type: "text", text: JSON.stringify({ error: "Unknown tool: " + toolName }) }],
219
+ isError: true
220
+ };
221
+ } catch (err) {
222
+ const msg = (err && err.message) || String(err);
223
+ return {
224
+ content: [{ type: "text", text: JSON.stringify({ error: msg }, null, 2) }],
225
+ isError: true
226
+ };
227
+ }
228
+ });
229
+
230
+ function logErr(...args) {
231
+ const msg = args.map((a) => (typeof a === "object" ? JSON.stringify(a) : String(a))).join(" ");
232
+ if (typeof process !== "undefined" && process.stderr && process.stderr.write) {
233
+ process.stderr.write("[word-generator-mcp] " + msg + "\n");
234
+ }
235
+ }
236
+ process.on("uncaughtException", (err) => {
237
+ logErr("uncaughtException:", err && err.message, err && err.stack);
238
+ });
239
+ process.on("unhandledRejection", (reason) => {
240
+ logErr("unhandledRejection:", reason);
241
+ });
242
+
243
+ const transport = new StdioServerTransport();
244
+ try {
245
+ await server.connect(transport);
246
+ } catch (err) {
247
+ logErr("server.connect failed:", err && err.message, err && err.stack);
248
+ process.exitCode = 1;
249
+ }