@xjtlumedia/markdown-mcp-server 1.0.2
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/dist/core-exports.js +545 -0
- package/dist/index.js +401 -0
- package/package.json +58 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { Paragraph, TextRun, HeadingLevel, Table, TableRow, TableCell, WidthType, BorderStyle, AlignmentType } from 'docx';
|
|
2
|
+
import * as XLSX from 'xlsx';
|
|
3
|
+
// ... (previous code)
|
|
4
|
+
/**
|
|
5
|
+
* Export table content only as CSV string
|
|
6
|
+
*/
|
|
7
|
+
export function generateCSV(content) {
|
|
8
|
+
const tableLines = content.match(/\|.*\|/g);
|
|
9
|
+
if (!tableLines)
|
|
10
|
+
return "";
|
|
11
|
+
let csv = "";
|
|
12
|
+
tableLines.forEach(line => {
|
|
13
|
+
if (line.includes('---'))
|
|
14
|
+
return;
|
|
15
|
+
const cells = line.split('|').map(c => c.trim()).filter(c => c !== "").map(c => stripMarkdown(c));
|
|
16
|
+
if (cells.length > 0)
|
|
17
|
+
csv += cells.map(c => `"${c.replace(/"/g, '""')}"`).join(',') + "\n";
|
|
18
|
+
});
|
|
19
|
+
return csv;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate JSON string representation
|
|
23
|
+
*/
|
|
24
|
+
export function generateJSON(content, title = 'document') {
|
|
25
|
+
const data = {
|
|
26
|
+
title: title,
|
|
27
|
+
export_timestamp: new Date().toISOString(),
|
|
28
|
+
content: stripMarkdown(content),
|
|
29
|
+
structured_content: content.split('\n\n').map(block => stripMarkdown(block))
|
|
30
|
+
};
|
|
31
|
+
return JSON.stringify(data, null, 2);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate XML string representation
|
|
35
|
+
*/
|
|
36
|
+
export function generateXML(content, title = 'document') {
|
|
37
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<document>\n`;
|
|
38
|
+
xml += ` <title>${title}</title>\n`;
|
|
39
|
+
xml += ` <content><![CDATA[${stripMarkdown(content)}]]></content>\n`;
|
|
40
|
+
xml += ` <metadata>\n <timestamp>${new Date().toISOString()}</timestamp>\n </metadata>\n`;
|
|
41
|
+
xml += `</document>`;
|
|
42
|
+
return xml;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parse markdown content to a 2D array of strings for table-like representations (CSV, XLSX)
|
|
46
|
+
*/
|
|
47
|
+
export function parseMarkdownToTableData(content) {
|
|
48
|
+
const tableData = [];
|
|
49
|
+
const paragraphs = content.split('\n\n');
|
|
50
|
+
paragraphs.forEach(para => {
|
|
51
|
+
const lines = para.trim().split('\n');
|
|
52
|
+
const isTable = lines.some(l => l.includes('|'));
|
|
53
|
+
if (isTable) {
|
|
54
|
+
lines.forEach(line => {
|
|
55
|
+
if (line.includes('---'))
|
|
56
|
+
return;
|
|
57
|
+
const cells = line.split('|').map(c => c.trim()).filter(c => c !== "").map(c => stripMarkdown(c));
|
|
58
|
+
if (cells.length > 0)
|
|
59
|
+
tableData.push(cells);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const cleaned = stripMarkdown(para);
|
|
64
|
+
if (cleaned)
|
|
65
|
+
tableData.push([cleaned]);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return tableData;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Generate XLSX Buffer
|
|
72
|
+
*/
|
|
73
|
+
export function generateXLSXIndex(content) {
|
|
74
|
+
const tableData = parseMarkdownToTableData(content);
|
|
75
|
+
const ws = XLSX.utils.aoa_to_sheet(tableData);
|
|
76
|
+
const wb = XLSX.utils.book_new();
|
|
77
|
+
XLSX.utils.book_append_sheet(wb, ws, "Document");
|
|
78
|
+
// Write to buffer
|
|
79
|
+
return XLSX.write(wb, { bookType: 'xlsx', type: 'buffer' });
|
|
80
|
+
}
|
|
81
|
+
export function stripMarkdown(text) {
|
|
82
|
+
if (!text)
|
|
83
|
+
return "";
|
|
84
|
+
let clean = text;
|
|
85
|
+
// 1. Block Level: Code Blocks
|
|
86
|
+
clean = clean.replace(/```[\s\S]*?```/g, m => m.replace(/```\w*\n?/g, '').replace(/```/g, '').trim());
|
|
87
|
+
// 2. Block Level: Tables (Separator rows)
|
|
88
|
+
clean = clean.replace(/^\|?[\s-:]+\|[\s-:|]*$/gm, '');
|
|
89
|
+
// 3. Block Level: Horizontal Rules & Alternate Headings
|
|
90
|
+
clean = clean.replace(/^[\s\t]*([*_-])\1{2,}\s*$/gm, '');
|
|
91
|
+
clean = clean.replace(/^[\s\t]*[=-]{3,}\s*$/gm, '');
|
|
92
|
+
// 4. Block Level: Blockquotes & ATX Headings
|
|
93
|
+
clean = clean.replace(/^[\s\t]*>+\s?/gm, '');
|
|
94
|
+
clean = clean.replace(/^#{1,6}\s+/gm, '');
|
|
95
|
+
// 5. Inline: Multi-pass Emphasis (Bold, Italic, Strikethrough)
|
|
96
|
+
for (let i = 0; i < 3; i++) {
|
|
97
|
+
clean = clean.replace(/[*_]{3}([^*_]+)[*_]{3}/g, '$1');
|
|
98
|
+
clean = clean.replace(/[*_]{2}([^*_]+)[*_]{2}/g, '$1');
|
|
99
|
+
clean = clean.replace(/[*_]{1}([^*_]+)[*_]{1}/g, '$1');
|
|
100
|
+
clean = clean.replace(/~~([^~]+)~~/g, '$1');
|
|
101
|
+
}
|
|
102
|
+
// 6. Inline: Math, Links, Images, and Extended Syntax
|
|
103
|
+
clean = clean.replace(/\$\$(.*?)\$\$/gs, '$1');
|
|
104
|
+
clean = clean.replace(/\$(.*?)\$/g, '$1');
|
|
105
|
+
clean = clean.replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1');
|
|
106
|
+
clean = clean.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
107
|
+
clean = clean.replace(/\[([^\]]+)\]\[[^\]]*\]/g, '$1');
|
|
108
|
+
clean = clean.replace(/\[[ xX]\]\s+/g, '');
|
|
109
|
+
clean = clean.replace(/\[\^[^\]]+\]/g, '');
|
|
110
|
+
clean = clean.replace(/\{#[^}]+\}/g, '');
|
|
111
|
+
clean = clean.replace(/[~^]([^~^]+)[~^]/g, '$1');
|
|
112
|
+
// 7. Inline: Code & HTML
|
|
113
|
+
clean = clean.replace(/`([^`]+)`/g, '$1');
|
|
114
|
+
clean = clean.replace(/<[^>]*>/g, '');
|
|
115
|
+
// 8. Final Polish: Pipes & Escaped Chars
|
|
116
|
+
clean = clean.replace(/\|/g, ' ');
|
|
117
|
+
clean = clean.replace(/\\([\\`*_{}[\]()#+\-.!|~^])/g, '$1');
|
|
118
|
+
// 9. Normalization
|
|
119
|
+
return clean
|
|
120
|
+
.split('\n')
|
|
121
|
+
.map(line => line.trim())
|
|
122
|
+
.join('\n')
|
|
123
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
124
|
+
.trim();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Parse markdown table to structured data
|
|
128
|
+
*/
|
|
129
|
+
export function parseMarkdownTable(tableText) {
|
|
130
|
+
const lines = tableText.trim().split('\n').filter(line => line.trim());
|
|
131
|
+
if (lines.length < 2)
|
|
132
|
+
return { headers: [], rows: [] };
|
|
133
|
+
const headers = lines[0].split('|').map(c => c.trim()).filter(c => c);
|
|
134
|
+
const rows = [];
|
|
135
|
+
for (let i = 2; i < lines.length; i++) {
|
|
136
|
+
const cells = lines[i].split('|').map(c => c.trim()).filter(c => c);
|
|
137
|
+
if (cells.length > 0)
|
|
138
|
+
rows.push(cells);
|
|
139
|
+
}
|
|
140
|
+
return { headers, rows };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Clean text by removing markdown symbols
|
|
144
|
+
*/
|
|
145
|
+
export function cleanMarkdownText(text) {
|
|
146
|
+
return stripMarkdown(text);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Parse markdown content to LaTeX
|
|
150
|
+
*/
|
|
151
|
+
export function parseMarkdownToLaTeX(content) {
|
|
152
|
+
let processed = content
|
|
153
|
+
.replace(/^# (.*)$/gm, '\\section{$1}')
|
|
154
|
+
.replace(/^## (.*)$/gm, '\\subsection{$1}')
|
|
155
|
+
.replace(/^### (.*)$/gm, '\\subsubsection{$1}')
|
|
156
|
+
.replace(/\*\*(.*)\*\*/g, '\\textbf{$1}')
|
|
157
|
+
.replace(/\*(.*)\*/g, '\\textit{$1}')
|
|
158
|
+
.replace(/\$\$(.*?)\$\$/gs, '\\begin{equation}\n$1\n\\end{equation}')
|
|
159
|
+
.replace(/\$(.*?)\$/g, '$ $1 $')
|
|
160
|
+
.replace(/^-\s(.*)$/gm, '\\begin{itemize}\n\\item $1\n\\end{itemize}')
|
|
161
|
+
.replace(/\\end{itemize}\n\\begin{itemize}/g, '');
|
|
162
|
+
// Escape LaTeX special chars but try not to break our commands
|
|
163
|
+
processed = processed.replace(/([_%$&~^\\{}])/g, (m) => m === '\\' ? m : `\\${m}`);
|
|
164
|
+
// Final pass to remove any markdown-only artifacts (hashes, backticks, pipe)
|
|
165
|
+
return processed.replace(/[*#`|]/g, '');
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse text with inline formatting to TextRuns
|
|
169
|
+
*/
|
|
170
|
+
export function parseInlineFormatting(text) {
|
|
171
|
+
const runs = [];
|
|
172
|
+
const regex = /(\$\$.*?\$\$|\$.*?\$|\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|___[^_]+___|__[^_]+__|_[^_]+_|`[^`]+`|<br\s*\/?>)/g;
|
|
173
|
+
const parts = text.split(regex);
|
|
174
|
+
for (const part of parts) {
|
|
175
|
+
if (!part)
|
|
176
|
+
continue;
|
|
177
|
+
if (part.startsWith('$$') && part.endsWith('$$')) {
|
|
178
|
+
runs.push(new TextRun({ text: part.slice(2, -2), italics: true, color: '4F46E5', font: 'Cambria Math' }));
|
|
179
|
+
}
|
|
180
|
+
else if (part.startsWith('$') && part.endsWith('$')) {
|
|
181
|
+
runs.push(new TextRun({ text: part.slice(1, -1), italics: true, color: '4F46E5', font: 'Cambria Math' }));
|
|
182
|
+
}
|
|
183
|
+
else if (part.startsWith('***') && part.endsWith('***')) {
|
|
184
|
+
runs.push(new TextRun({ text: part.slice(3, -3), bold: true, italics: true }));
|
|
185
|
+
}
|
|
186
|
+
else if (part.startsWith('___') && part.endsWith('___')) {
|
|
187
|
+
runs.push(new TextRun({ text: part.slice(3, -3), bold: true, italics: true }));
|
|
188
|
+
}
|
|
189
|
+
else if (part.startsWith('**') && part.endsWith('**')) {
|
|
190
|
+
runs.push(new TextRun({ text: part.slice(2, -2), bold: true }));
|
|
191
|
+
}
|
|
192
|
+
else if (part.startsWith('__') && part.endsWith('__')) {
|
|
193
|
+
runs.push(new TextRun({ text: part.slice(2, -2), bold: true }));
|
|
194
|
+
}
|
|
195
|
+
else if (part.startsWith('*') && part.endsWith('*')) {
|
|
196
|
+
runs.push(new TextRun({ text: part.slice(1, -1), italics: true }));
|
|
197
|
+
}
|
|
198
|
+
else if (part.startsWith('_') && part.endsWith('_')) {
|
|
199
|
+
runs.push(new TextRun({ text: part.slice(1, -1), italics: true }));
|
|
200
|
+
}
|
|
201
|
+
else if (part.startsWith('`') && part.endsWith('`')) {
|
|
202
|
+
runs.push(new TextRun({ text: part.slice(1, -1), font: 'Consolas', shading: { fill: 'F0F0F0' } }));
|
|
203
|
+
}
|
|
204
|
+
else if (part.match(/<br\s*\/?>/i)) {
|
|
205
|
+
runs.push(new TextRun({ text: '', break: 1 }));
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
runs.push(new TextRun({ text: part }));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return runs.length > 0 ? runs : [new TextRun({ text })];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Create a Word table from parsed markdown table data
|
|
215
|
+
*/
|
|
216
|
+
export function createDocxTable(headers, rows) {
|
|
217
|
+
const allRows = [];
|
|
218
|
+
if (headers.length > 0) {
|
|
219
|
+
allRows.push(new TableRow({
|
|
220
|
+
children: headers.map(header => new TableCell({
|
|
221
|
+
children: [new Paragraph({ children: [new TextRun({ text: cleanMarkdownText(header), bold: true })], alignment: AlignmentType.LEFT })],
|
|
222
|
+
shading: { fill: 'E5E7EB' }
|
|
223
|
+
}))
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
for (const row of rows) {
|
|
227
|
+
allRows.push(new TableRow({
|
|
228
|
+
children: row.map(cell => new TableCell({ children: [new Paragraph({ children: parseInlineFormatting(cell) })] }))
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
return new Table({
|
|
232
|
+
rows: allRows,
|
|
233
|
+
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
234
|
+
borders: {
|
|
235
|
+
top: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
|
|
236
|
+
bottom: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
|
|
237
|
+
left: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
|
|
238
|
+
right: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
|
|
239
|
+
insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' },
|
|
240
|
+
insideVertical: { style: BorderStyle.SINGLE, size: 1, color: 'CCCCCC' }
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Parse markdown content to docx elements
|
|
246
|
+
*/
|
|
247
|
+
export function parseMarkdownToDocx(content) {
|
|
248
|
+
const elements = [];
|
|
249
|
+
const lines = content.split('\n');
|
|
250
|
+
let i = 0;
|
|
251
|
+
let inCodeBlock = false;
|
|
252
|
+
let codeBlockContent = [];
|
|
253
|
+
while (i < lines.length) {
|
|
254
|
+
const line = lines[i];
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
if (/^(\*\*\*|---|__{3,})\s*$/.test(trimmed)) {
|
|
257
|
+
elements.push(new Paragraph({ border: { bottom: { color: 'CCCCCC', space: 1, style: BorderStyle.SINGLE, size: 6 } }, spacing: { before: 200, after: 200 } }));
|
|
258
|
+
i++;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (trimmed.startsWith('```')) {
|
|
262
|
+
if (inCodeBlock) {
|
|
263
|
+
elements.push(new Paragraph({ children: [new TextRun({ text: codeBlockContent.join('\n'), font: 'Consolas', size: 20 })], shading: { fill: 'F3F4F6' }, spacing: { before: 200, after: 200 } }));
|
|
264
|
+
codeBlockContent = [];
|
|
265
|
+
inCodeBlock = false;
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
inCodeBlock = true;
|
|
269
|
+
}
|
|
270
|
+
i++;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (inCodeBlock) {
|
|
274
|
+
codeBlockContent.push(line);
|
|
275
|
+
i++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (i + 1 < lines.length) {
|
|
279
|
+
const nextLine = lines[i + 1].trim();
|
|
280
|
+
if (/^={3,}\s*$/.test(nextLine)) {
|
|
281
|
+
elements.push(new Paragraph({ heading: HeadingLevel.HEADING_1, children: parseInlineFormatting(trimmed), spacing: { before: 400, after: 200 } }));
|
|
282
|
+
i += 2;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
else if (/^-{3,}\s*$/.test(nextLine)) {
|
|
286
|
+
elements.push(new Paragraph({ heading: HeadingLevel.HEADING_2, children: parseInlineFormatting(trimmed), spacing: { before: 300, after: 150 } }));
|
|
287
|
+
i += 2;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (trimmed.includes('|') && trimmed.startsWith('|')) {
|
|
292
|
+
const tableLines = [];
|
|
293
|
+
while (i < lines.length && lines[i].trim().includes('|')) {
|
|
294
|
+
tableLines.push(lines[i]);
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
if (tableLines.length >= 2) {
|
|
298
|
+
const { headers, rows } = parseMarkdownTable(tableLines.join('\n'));
|
|
299
|
+
if (headers.length > 0) {
|
|
300
|
+
elements.push(createDocxTable(headers, rows));
|
|
301
|
+
elements.push(new Paragraph({ spacing: { after: 200 } }));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (!trimmed) {
|
|
307
|
+
elements.push(new Paragraph({ spacing: { after: 100 } }));
|
|
308
|
+
i++;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (trimmed.startsWith('# ')) {
|
|
312
|
+
elements.push(new Paragraph({ heading: HeadingLevel.HEADING_1, children: parseInlineFormatting(trimmed.slice(2)), spacing: { before: 400, after: 200 } }));
|
|
313
|
+
}
|
|
314
|
+
else if (trimmed.startsWith('## ')) {
|
|
315
|
+
elements.push(new Paragraph({ heading: HeadingLevel.HEADING_2, children: parseInlineFormatting(trimmed.slice(3)), spacing: { before: 300, after: 150 } }));
|
|
316
|
+
}
|
|
317
|
+
else if (trimmed.startsWith('### ')) {
|
|
318
|
+
elements.push(new Paragraph({ heading: HeadingLevel.HEADING_3, children: parseInlineFormatting(trimmed.slice(4)), spacing: { before: 250, after: 100 } }));
|
|
319
|
+
}
|
|
320
|
+
else if (trimmed.startsWith('#### ')) {
|
|
321
|
+
elements.push(new Paragraph({ heading: HeadingLevel.HEADING_4, children: parseInlineFormatting(trimmed.slice(5)), spacing: { before: 200, after: 100 } }));
|
|
322
|
+
}
|
|
323
|
+
else if (trimmed.startsWith('>')) {
|
|
324
|
+
const level = (trimmed.match(/^>+/g) || ['>'])[0].length;
|
|
325
|
+
const text = trimmed.replace(/^>+\s*/, '');
|
|
326
|
+
elements.push(new Paragraph({ indent: { left: 720 * level }, children: [new TextRun({ text: cleanMarkdownText(text), italics: true, color: '666666' })], spacing: { after: 100 }, shading: { fill: 'F9FAFB' } }));
|
|
327
|
+
}
|
|
328
|
+
else if (/^(\s*)[-*+]\s+/.test(line)) {
|
|
329
|
+
const match = line.match(/^(\s*)([-*+]\s+)/);
|
|
330
|
+
const indent = match ? Math.floor(match[1].length / 4) : 0;
|
|
331
|
+
const text = line.replace(/^\s*[-*+]\s+/, '');
|
|
332
|
+
elements.push(new Paragraph({ bullet: { level: indent }, children: parseInlineFormatting(text), spacing: { after: 80 } }));
|
|
333
|
+
}
|
|
334
|
+
else if (/^(\s*)\d+\.\s+/.test(line)) {
|
|
335
|
+
const match = line.match(/^(\s*)(\d+\.\s+)/);
|
|
336
|
+
const indent = match ? Math.floor(match[1].length / 4) : 0;
|
|
337
|
+
const text = line.replace(/^\s*\d+\.\s+/, '');
|
|
338
|
+
elements.push(new Paragraph({ numbering: { reference: 'default-numbering', level: indent }, children: parseInlineFormatting(text), spacing: { after: 80 } }));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const paragraphChildren = parseInlineFormatting(trimmed);
|
|
342
|
+
if (line.endsWith(' '))
|
|
343
|
+
paragraphChildren.push(new TextRun({ text: '', break: 1 }));
|
|
344
|
+
elements.push(new Paragraph({ children: paragraphChildren, spacing: { after: 150 } }));
|
|
345
|
+
}
|
|
346
|
+
i++;
|
|
347
|
+
}
|
|
348
|
+
return elements;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* RTF Helper: Encode string with Unicode support and RTF escaping
|
|
352
|
+
*/
|
|
353
|
+
export function encodeRTFText(str) {
|
|
354
|
+
let res = "";
|
|
355
|
+
for (let i = 0; i < str.length; i++) {
|
|
356
|
+
const charCode = str.charCodeAt(i);
|
|
357
|
+
if (charCode > 127) {
|
|
358
|
+
res += `\\u${charCode}?`;
|
|
359
|
+
}
|
|
360
|
+
else if (str[i] === '\\' || str[i] === '{' || str[i] === '}') {
|
|
361
|
+
res += '\\' + str[i];
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
res += str[i];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return res;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* RTF Helper: Parse inline markdown to RTF codes
|
|
371
|
+
*/
|
|
372
|
+
export function parseInlineToRTF(text) {
|
|
373
|
+
const regex = /(\$\$.*?\$\$|\$.*?\$|\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*|___[^_]+___|__[^_]+__|_[^_]+_|`[^`]+`|<br\s*\/?>)/g;
|
|
374
|
+
const parts = text.split(regex);
|
|
375
|
+
let result = "";
|
|
376
|
+
for (const part of parts) {
|
|
377
|
+
if (!part)
|
|
378
|
+
continue;
|
|
379
|
+
if (part.startsWith('$$') && part.endsWith('$$')) {
|
|
380
|
+
result += `{\\i\\cf4\\f2 ${encodeRTFText(part.slice(2, -2))}}`;
|
|
381
|
+
}
|
|
382
|
+
else if (part.startsWith('$') && part.endsWith('$')) {
|
|
383
|
+
result += `{\\i\\cf4\\f2 ${encodeRTFText(part.slice(1, -1))}}`;
|
|
384
|
+
}
|
|
385
|
+
else if (part.startsWith('***') && part.endsWith('***')) {
|
|
386
|
+
result += `{\\b\\i ${encodeRTFText(part.slice(3, -3))}}`;
|
|
387
|
+
}
|
|
388
|
+
else if (part.startsWith('___') && part.endsWith('___')) {
|
|
389
|
+
result += `{\\b\\i ${encodeRTFText(part.slice(3, -3))}}`;
|
|
390
|
+
}
|
|
391
|
+
else if (part.startsWith('**') && part.endsWith('**')) {
|
|
392
|
+
result += `{\\b ${encodeRTFText(part.slice(2, -2))}}`;
|
|
393
|
+
}
|
|
394
|
+
else if (part.startsWith('__') && part.endsWith('__')) {
|
|
395
|
+
result += `{\\b ${encodeRTFText(part.slice(2, -2))}}`;
|
|
396
|
+
}
|
|
397
|
+
else if (part.startsWith('*') && part.endsWith('*')) {
|
|
398
|
+
result += `{\\i ${encodeRTFText(part.slice(1, -1))}}`;
|
|
399
|
+
}
|
|
400
|
+
else if (part.startsWith('_') && part.endsWith('_')) {
|
|
401
|
+
result += `{\\i ${encodeRTFText(part.slice(1, -1))}}`;
|
|
402
|
+
}
|
|
403
|
+
else if (part.startsWith('`') && part.endsWith('`')) {
|
|
404
|
+
result += `{\\f1\\highlight3 ${encodeRTFText(part.slice(1, -1))}}`;
|
|
405
|
+
}
|
|
406
|
+
else if (part.match(/<br\s*\/?>/i)) {
|
|
407
|
+
result += "\\line ";
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
result += encodeRTFText(part);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* RTF Helper: Main parser for Markdown to RTF conversion
|
|
417
|
+
*/
|
|
418
|
+
export function parseMarkdownToRTF(content) {
|
|
419
|
+
const lines = content.split('\n');
|
|
420
|
+
let rtf = "";
|
|
421
|
+
let i = 0;
|
|
422
|
+
let inCodeBlock = false;
|
|
423
|
+
let codeBlockContent = [];
|
|
424
|
+
while (i < lines.length) {
|
|
425
|
+
const line = lines[i];
|
|
426
|
+
const trimmed = line.trim();
|
|
427
|
+
// Horizontal Rule
|
|
428
|
+
if (/^(\*\*\*|---|__{3,})\s*$/.test(trimmed)) {
|
|
429
|
+
rtf += "\\pard\\sb200\\sa200\\brdrb\\brdrs\\brdrw10\\brdrcf6\\par\n";
|
|
430
|
+
i++;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
// Code Block
|
|
434
|
+
if (trimmed.startsWith('```')) {
|
|
435
|
+
if (inCodeBlock) {
|
|
436
|
+
rtf += "{\\pard\\f1\\fs20\\highlight3 " + encodeRTFText(codeBlockContent.join("\\line\n")) + "\\par}\n";
|
|
437
|
+
codeBlockContent = [];
|
|
438
|
+
inCodeBlock = false;
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
inCodeBlock = true;
|
|
442
|
+
}
|
|
443
|
+
i++;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (inCodeBlock) {
|
|
447
|
+
codeBlockContent.push(line);
|
|
448
|
+
i++;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
// Setext Headings
|
|
452
|
+
if (i + 1 < lines.length) {
|
|
453
|
+
const nextLine = lines[i + 1].trim();
|
|
454
|
+
if (/^={3,}\s*$/.test(nextLine)) {
|
|
455
|
+
rtf += "{\\pard\\b\\fs40\\sb400\\sa200 " + parseInlineToRTF(trimmed) + "\\par}\n";
|
|
456
|
+
i += 2;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
else if (/^-{3,}\s*$/.test(nextLine)) {
|
|
460
|
+
rtf += "{\\pard\\b\\fs32\\sb300\\sa150 " + parseInlineToRTF(trimmed) + "\\par}\n";
|
|
461
|
+
i += 2;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Tables
|
|
466
|
+
if (trimmed.includes('|') && trimmed.startsWith('|')) {
|
|
467
|
+
const tableLines = [];
|
|
468
|
+
while (i < lines.length && lines[i].trim().includes('|')) {
|
|
469
|
+
tableLines.push(lines[i]);
|
|
470
|
+
i++;
|
|
471
|
+
}
|
|
472
|
+
if (tableLines.length >= 2) {
|
|
473
|
+
const { headers, rows } = parseMarkdownTable(tableLines.join('\n'));
|
|
474
|
+
if (headers.length > 0) {
|
|
475
|
+
const cellWidth = 3000;
|
|
476
|
+
// Header Row
|
|
477
|
+
rtf += "\\trowd\\trgaph108\\trleft-108";
|
|
478
|
+
for (let j = 0; j < headers.length; j++) {
|
|
479
|
+
rtf += `\\clcbpat5\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10\\cellx${(j + 1) * cellWidth}`;
|
|
480
|
+
}
|
|
481
|
+
rtf += "\\pard\\intbl\\ql ";
|
|
482
|
+
for (const h of headers) {
|
|
483
|
+
rtf += "{\\b " + parseInlineToRTF(h) + "}\\cell ";
|
|
484
|
+
}
|
|
485
|
+
rtf += "\\row\n";
|
|
486
|
+
// Data Rows
|
|
487
|
+
for (const row of rows) {
|
|
488
|
+
rtf += "\\trowd\\trgaph108\\trleft-108";
|
|
489
|
+
for (let j = 0; j < row.length; j++) {
|
|
490
|
+
rtf += `\\clbrdrt\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10\\cellx${(j + 1) * cellWidth}`;
|
|
491
|
+
}
|
|
492
|
+
rtf += "\\pard\\intbl\\ql ";
|
|
493
|
+
for (const cell of row) {
|
|
494
|
+
rtf += parseInlineToRTF(cell) + "\\cell ";
|
|
495
|
+
}
|
|
496
|
+
rtf += "\\row\n";
|
|
497
|
+
}
|
|
498
|
+
rtf += "\\pard\\sa200\\par\n";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (!trimmed) {
|
|
504
|
+
rtf += "\\pard\\sa100\\par\n";
|
|
505
|
+
i++;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
// Headings
|
|
509
|
+
if (trimmed.startsWith('# ')) {
|
|
510
|
+
rtf += "{\\pard\\b\\fs40\\sb400\\sa200 " + parseInlineToRTF(trimmed.slice(2)) + "\\par}\n";
|
|
511
|
+
}
|
|
512
|
+
else if (trimmed.startsWith('## ')) {
|
|
513
|
+
rtf += "{\\pard\\b\\fs32\\sb300\\sa150 " + parseInlineToRTF(trimmed.slice(3)) + "\\par}\n";
|
|
514
|
+
}
|
|
515
|
+
else if (trimmed.startsWith('### ')) {
|
|
516
|
+
rtf += "{\\pard\\b\\fs28\\sb250\\sa100 " + parseInlineToRTF(trimmed.slice(4)) + "\\par}\n";
|
|
517
|
+
}
|
|
518
|
+
else if (trimmed.startsWith('#### ')) {
|
|
519
|
+
rtf += "{\\pard\\b\\fs26\\sb200\\sa100 " + parseInlineToRTF(trimmed.slice(5)) + "\\par}\n";
|
|
520
|
+
}
|
|
521
|
+
else if (trimmed.startsWith('>')) {
|
|
522
|
+
const level = (trimmed.match(/^>+/g) || ['>'])[0].length;
|
|
523
|
+
const text = trimmed.replace(/^>+\s*/, '');
|
|
524
|
+
rtf += `{\\pard\\li${level * 720}\\cf2\\i\\sa100 ` + parseInlineToRTF(text) + "\\par}\n";
|
|
525
|
+
}
|
|
526
|
+
else if (/^(\s*)[-*+]\s+/.test(line)) {
|
|
527
|
+
const match = line.match(/^(\s*)([-*+]\s+)/);
|
|
528
|
+
const indent = match ? Math.floor(match[1].length / 4) : 0;
|
|
529
|
+
const text = line.replace(/^\s*[-*+]\s+/, '');
|
|
530
|
+
rtf += `{\\pard\\li${(indent + 1) * 360}\\fi-360\\'b7\\tab ` + parseInlineToRTF(text) + "\\par}\n";
|
|
531
|
+
}
|
|
532
|
+
else if (/^(\s*)\d+\.\s+/.test(line)) {
|
|
533
|
+
const match = line.match(/^(\s*)(\d+\.\s+)/);
|
|
534
|
+
const indent = match ? Math.floor(match[1].length / 4) : 0;
|
|
535
|
+
const number = match ? match[2] : "1. ";
|
|
536
|
+
const text = line.replace(/^\s*\d+\.\s+/, '');
|
|
537
|
+
rtf += `{\\pard\\li${(indent + 1) * 360}\\fi-360 ${number}\\tab ` + parseInlineToRTF(text) + "\\par}\n";
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
rtf += "{\\pard\\sa150 " + parseInlineToRTF(trimmed) + "\\par}\n";
|
|
541
|
+
}
|
|
542
|
+
i++;
|
|
543
|
+
}
|
|
544
|
+
return rtf;
|
|
545
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import remarkParse from 'remark-parse';
|
|
8
|
+
import remarkGfm from 'remark-gfm';
|
|
9
|
+
import remarkMath from 'remark-math';
|
|
10
|
+
import remarkStringify from 'remark-stringify';
|
|
11
|
+
import remarkRehype from 'remark-rehype';
|
|
12
|
+
import rehypeKatex from 'rehype-katex';
|
|
13
|
+
import rehypeStringify from 'rehype-stringify';
|
|
14
|
+
import puppeteer from 'puppeteer';
|
|
15
|
+
import * as fs from 'fs/promises';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { parseMarkdownToRTF, parseMarkdownToDocx, parseMarkdownToLaTeX, generateCSV, generateJSON, generateXML, generateXLSXIndex, cleanMarkdownText } from "./core-exports.js";
|
|
18
|
+
import { Packer } from "docx";
|
|
19
|
+
const server = new Server({
|
|
20
|
+
name: "markdown-formatter-mcp",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
}, {
|
|
23
|
+
capabilities: {
|
|
24
|
+
tools: {},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
// Binary format types that need special handling
|
|
28
|
+
const BINARY_FORMATS = ['docx', 'pdf', 'xlsx', 'png', 'image'];
|
|
29
|
+
// Helper to handle output (save to file or return content)
|
|
30
|
+
async function handleOutput(content, outputPath, options) {
|
|
31
|
+
if (outputPath) {
|
|
32
|
+
try {
|
|
33
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
34
|
+
await fs.writeFile(outputPath, content);
|
|
35
|
+
const stats = await fs.stat(outputPath);
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: JSON.stringify({
|
|
40
|
+
success: true,
|
|
41
|
+
message: `Successfully saved to ${outputPath}`,
|
|
42
|
+
file_path: outputPath,
|
|
43
|
+
file_size_bytes: stats.size,
|
|
44
|
+
format: options?.format || 'unknown'
|
|
45
|
+
}, null, 2)
|
|
46
|
+
}]
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return { content: [{ type: "text", text: `Error saving to file: ${err.message}` }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// For binary content without output_path, return helpful guidance
|
|
54
|
+
if (Buffer.isBuffer(content)) {
|
|
55
|
+
const sizeBytes = content.length;
|
|
56
|
+
const format = options?.format || 'binary';
|
|
57
|
+
// For AI usability, don't dump raw Base64 - provide actionable guidance
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: JSON.stringify({
|
|
62
|
+
success: true,
|
|
63
|
+
format: format,
|
|
64
|
+
file_size_bytes: sizeBytes,
|
|
65
|
+
description: options?.description || `Generated ${format.toUpperCase()} binary content`,
|
|
66
|
+
hint: `This is a binary file format. To save the file, call this tool again with the 'output_path' parameter specifying where to save it (e.g., "C:/Documents/output.${format}" or "./output.${format}").`,
|
|
67
|
+
base64_preview: content.toString('base64').substring(0, 100) + '...',
|
|
68
|
+
full_base64_length: content.toString('base64').length
|
|
69
|
+
}, null, 2)
|
|
70
|
+
}]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
return { content: [{ type: "text", text: content }] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
78
|
+
return {
|
|
79
|
+
tools: [
|
|
80
|
+
{
|
|
81
|
+
name: "harmonize_markdown",
|
|
82
|
+
description: "Standardize markdown syntax (headers, list markers, etc.)",
|
|
83
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
84
|
+
markdown: z.string(),
|
|
85
|
+
output_path: z.string().optional(),
|
|
86
|
+
})),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "convert_to_txt",
|
|
90
|
+
description: "Convert Markdown to Plain Text (strips formatting)",
|
|
91
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
92
|
+
markdown: z.string(),
|
|
93
|
+
output_path: z.string().optional(),
|
|
94
|
+
})),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "convert_to_rtf",
|
|
98
|
+
description: "Convert Markdown to RTF (Rich Text Format)",
|
|
99
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
100
|
+
markdown: z.string(),
|
|
101
|
+
output_path: z.string().optional(),
|
|
102
|
+
})),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "convert_to_latex",
|
|
106
|
+
description: "Convert Markdown to LaTeX",
|
|
107
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
108
|
+
markdown: z.string(),
|
|
109
|
+
output_path: z.string().optional(),
|
|
110
|
+
})),
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: "convert_to_docx",
|
|
114
|
+
description: "Convert Markdown to DOCX (Word)",
|
|
115
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
116
|
+
markdown: z.string(),
|
|
117
|
+
output_path: z.string().optional(),
|
|
118
|
+
})),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: "convert_to_pdf",
|
|
122
|
+
description: "Convert Markdown to PDF (uses Puppeteer)",
|
|
123
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
124
|
+
markdown: z.string(),
|
|
125
|
+
output_path: z.string().optional(),
|
|
126
|
+
})),
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "convert_to_image",
|
|
130
|
+
description: "Convert Markdown to PNG Image (uses Puppeteer)",
|
|
131
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
132
|
+
markdown: z.string(),
|
|
133
|
+
output_path: z.string().optional(),
|
|
134
|
+
})),
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "convert_to_csv",
|
|
138
|
+
description: "Extract tables from Markdown to CSV",
|
|
139
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
140
|
+
markdown: z.string(),
|
|
141
|
+
output_path: z.string().optional(),
|
|
142
|
+
})),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "convert_to_json",
|
|
146
|
+
description: "Convert Markdown to JSON structure",
|
|
147
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
148
|
+
markdown: z.string(),
|
|
149
|
+
title: z.string().optional(),
|
|
150
|
+
output_path: z.string().optional(),
|
|
151
|
+
})),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "convert_to_xml",
|
|
155
|
+
description: "Convert Markdown to XML",
|
|
156
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
157
|
+
markdown: z.string(),
|
|
158
|
+
title: z.string().optional(),
|
|
159
|
+
output_path: z.string().optional(),
|
|
160
|
+
})),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "convert_to_xlsx",
|
|
164
|
+
description: "Convert Markdown tables to Excel (XLSX)",
|
|
165
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
166
|
+
markdown: z.string(),
|
|
167
|
+
output_path: z.string().optional(),
|
|
168
|
+
})),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "convert_to_html",
|
|
172
|
+
description: "Convert Markdown to HTML",
|
|
173
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
174
|
+
markdown: z.string(),
|
|
175
|
+
output_path: z.string().optional(),
|
|
176
|
+
})),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "convert_to_md",
|
|
180
|
+
description: "Export original Markdown content (with optional harmonization)",
|
|
181
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
182
|
+
markdown: z.string(),
|
|
183
|
+
harmonize: z.boolean().optional(),
|
|
184
|
+
output_path: z.string().optional(),
|
|
185
|
+
})),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "generate_html",
|
|
189
|
+
description: "Generate a complete HTML document from Markdown with inline styles (returns full HTML string)",
|
|
190
|
+
inputSchema: zodSchemaToToolInput(z.object({
|
|
191
|
+
markdown: z.string(),
|
|
192
|
+
title: z.string().optional(),
|
|
193
|
+
})),
|
|
194
|
+
}
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
199
|
+
try {
|
|
200
|
+
const { name, arguments: args } = request.params;
|
|
201
|
+
const markdown = args.markdown;
|
|
202
|
+
const outputPath = args.output_path;
|
|
203
|
+
if (!markdown && name !== 'list_tools') {
|
|
204
|
+
// Basic validation
|
|
205
|
+
throw new Error("Markdown content is required");
|
|
206
|
+
}
|
|
207
|
+
if (name === "harmonize_markdown") {
|
|
208
|
+
const file = await unified()
|
|
209
|
+
.use(remarkParse)
|
|
210
|
+
.use(remarkGfm)
|
|
211
|
+
.use(remarkMath)
|
|
212
|
+
.use(remarkStringify, {
|
|
213
|
+
bullet: '-',
|
|
214
|
+
fence: '`',
|
|
215
|
+
fences: true,
|
|
216
|
+
incrementListMarker: true,
|
|
217
|
+
listItemIndent: 'one',
|
|
218
|
+
})
|
|
219
|
+
.process(markdown);
|
|
220
|
+
return handleOutput(String(file), outputPath);
|
|
221
|
+
}
|
|
222
|
+
if (name === "convert_to_txt") {
|
|
223
|
+
const txt = cleanMarkdownText(markdown);
|
|
224
|
+
return handleOutput(txt, outputPath);
|
|
225
|
+
}
|
|
226
|
+
if (name === "convert_to_rtf") {
|
|
227
|
+
const rtf = parseMarkdownToRTF(markdown);
|
|
228
|
+
return handleOutput(rtf, outputPath);
|
|
229
|
+
}
|
|
230
|
+
if (name === "convert_to_latex") {
|
|
231
|
+
const latex = parseMarkdownToLaTeX(markdown);
|
|
232
|
+
return handleOutput(latex, outputPath);
|
|
233
|
+
}
|
|
234
|
+
if (name === "convert_to_docx") {
|
|
235
|
+
const elements = parseMarkdownToDocx(markdown);
|
|
236
|
+
const doc = new ((await import("docx")).Document)({
|
|
237
|
+
sections: [{ children: elements }]
|
|
238
|
+
});
|
|
239
|
+
const buffer = await Packer.toBuffer(doc);
|
|
240
|
+
return handleOutput(buffer, outputPath, {
|
|
241
|
+
format: 'docx',
|
|
242
|
+
description: 'Microsoft Word document generated from Markdown'
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (name === "convert_to_csv") {
|
|
246
|
+
const csv = generateCSV(markdown);
|
|
247
|
+
return handleOutput(csv, outputPath);
|
|
248
|
+
}
|
|
249
|
+
if (name === "convert_to_json") {
|
|
250
|
+
const title = args.title || "document";
|
|
251
|
+
const json = generateJSON(markdown, title);
|
|
252
|
+
return handleOutput(json, outputPath);
|
|
253
|
+
}
|
|
254
|
+
if (name === "convert_to_xml") {
|
|
255
|
+
const title = args.title || "document";
|
|
256
|
+
const xml = generateXML(markdown, title);
|
|
257
|
+
return handleOutput(xml, outputPath);
|
|
258
|
+
}
|
|
259
|
+
if (name === "convert_to_xlsx") {
|
|
260
|
+
const buffer = generateXLSXIndex(markdown);
|
|
261
|
+
return handleOutput(buffer, outputPath, {
|
|
262
|
+
format: 'xlsx',
|
|
263
|
+
description: 'Microsoft Excel spreadsheet generated from Markdown tables'
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
if (name === "convert_to_html" || name === "convert_to_pdf" || name === "convert_to_image") {
|
|
267
|
+
const htmlProcessor = unified()
|
|
268
|
+
.use(remarkParse)
|
|
269
|
+
.use(remarkGfm)
|
|
270
|
+
// @ts-ignore
|
|
271
|
+
.use(remarkRehype)
|
|
272
|
+
// @ts-ignore
|
|
273
|
+
.use(rehypeKatex)
|
|
274
|
+
// @ts-ignore
|
|
275
|
+
.use(rehypeStringify);
|
|
276
|
+
const htmlFile = await htmlProcessor.process(markdown);
|
|
277
|
+
const htmlDoc = `<!DOCTYPE html>
|
|
278
|
+
<html>
|
|
279
|
+
<head>
|
|
280
|
+
<meta charset="utf-8">
|
|
281
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous">
|
|
282
|
+
<style>
|
|
283
|
+
body { font-family: system-ui, -apple-system, sans-serif; padding: 40px; line-height: 1.6; max-width: 800px; margin: 0 auto; background: white; color: black; }
|
|
284
|
+
img { max-width: 100%; }
|
|
285
|
+
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
|
286
|
+
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
|
287
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
288
|
+
th { background-color: #f2f2f2; }
|
|
289
|
+
</style>
|
|
290
|
+
</head>
|
|
291
|
+
<body>${String(htmlFile)}</body>
|
|
292
|
+
</html>`;
|
|
293
|
+
if (name === "convert_to_html") {
|
|
294
|
+
return handleOutput(htmlDoc, outputPath);
|
|
295
|
+
}
|
|
296
|
+
const browser = await puppeteer.launch({ headless: true });
|
|
297
|
+
const page = await browser.newPage();
|
|
298
|
+
await page.setContent(htmlDoc);
|
|
299
|
+
let resultBuffer;
|
|
300
|
+
if (name === "convert_to_pdf") {
|
|
301
|
+
resultBuffer = await page.pdf({ format: 'A4' });
|
|
302
|
+
await browser.close();
|
|
303
|
+
return handleOutput(resultBuffer, outputPath, {
|
|
304
|
+
format: 'pdf',
|
|
305
|
+
description: 'PDF document generated from Markdown via Puppeteer'
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
const screenshot = await page.screenshot({ fullPage: true, encoding: 'binary' });
|
|
310
|
+
resultBuffer = screenshot;
|
|
311
|
+
await browser.close();
|
|
312
|
+
return handleOutput(resultBuffer, outputPath, {
|
|
313
|
+
format: 'png',
|
|
314
|
+
description: 'PNG image screenshot of the rendered Markdown'
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// New tools: convert_to_md and generate_html
|
|
319
|
+
if (name === "convert_to_md") {
|
|
320
|
+
const shouldHarmonize = args.harmonize;
|
|
321
|
+
let result = markdown;
|
|
322
|
+
if (shouldHarmonize) {
|
|
323
|
+
const file = await unified()
|
|
324
|
+
.use(remarkParse)
|
|
325
|
+
.use(remarkGfm)
|
|
326
|
+
.use(remarkMath)
|
|
327
|
+
.use(remarkStringify, {
|
|
328
|
+
bullet: '-',
|
|
329
|
+
fence: '`',
|
|
330
|
+
fences: true,
|
|
331
|
+
incrementListMarker: true,
|
|
332
|
+
listItemIndent: 'one',
|
|
333
|
+
})
|
|
334
|
+
.process(markdown);
|
|
335
|
+
result = String(file);
|
|
336
|
+
}
|
|
337
|
+
return handleOutput(result, outputPath);
|
|
338
|
+
}
|
|
339
|
+
if (name === "generate_html") {
|
|
340
|
+
const title = args.title || 'Document';
|
|
341
|
+
const htmlProcessor = unified()
|
|
342
|
+
.use(remarkParse)
|
|
343
|
+
.use(remarkGfm)
|
|
344
|
+
// @ts-ignore
|
|
345
|
+
.use(remarkRehype)
|
|
346
|
+
// @ts-ignore
|
|
347
|
+
.use(rehypeKatex)
|
|
348
|
+
// @ts-ignore
|
|
349
|
+
.use(rehypeStringify);
|
|
350
|
+
const htmlFile = await htmlProcessor.process(markdown);
|
|
351
|
+
const htmlDoc = `<!DOCTYPE html>
|
|
352
|
+
<html lang="en">
|
|
353
|
+
<head>
|
|
354
|
+
<meta charset="UTF-8">
|
|
355
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
356
|
+
<title>${title}</title>
|
|
357
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
|
358
|
+
<style>
|
|
359
|
+
body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; line-height: 1.6; color: #1a1a1a; }
|
|
360
|
+
h1, h2, h3 { color: #111; margin-top: 2em; }
|
|
361
|
+
pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
|
|
362
|
+
code { font-family: monospace; background: #eee; padding: 2px 4px; border-radius: 3px; }
|
|
363
|
+
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
|
364
|
+
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
|
365
|
+
th { background: #f8f8f8; }
|
|
366
|
+
blockquote { border-left: 4px solid #ddd; margin: 0; padding-left: 1em; color: #666; }
|
|
367
|
+
</style>
|
|
368
|
+
</head>
|
|
369
|
+
<body>${String(htmlFile)}</body>
|
|
370
|
+
</html>`;
|
|
371
|
+
return { content: [{ type: "text", text: htmlDoc }] };
|
|
372
|
+
}
|
|
373
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: `Error: ${error.message}` }],
|
|
378
|
+
isError: true,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
function zodSchemaToToolInput(schema) {
|
|
383
|
+
const shape = schema.shape;
|
|
384
|
+
const properties = {};
|
|
385
|
+
const required = [];
|
|
386
|
+
for (const key in shape) {
|
|
387
|
+
const field = shape[key];
|
|
388
|
+
const isOptional = field.isOptional?.() || field instanceof z.ZodOptional;
|
|
389
|
+
properties[key] = { type: "string" };
|
|
390
|
+
if (!isOptional) {
|
|
391
|
+
required.push(key);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
type: "object",
|
|
396
|
+
properties,
|
|
397
|
+
required
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const transport = new StdioServerTransport();
|
|
401
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xjtlumedia/markdown-mcp-server",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "MCP Server for Markdown Processing and Multi-Format Exporting — convert Markdown to HTML, PDF, DOCX, LaTeX, CSV, JSON, XML, XLSX, RTF, PNG and more",
|
|
5
|
+
"mcpName": "io.github.XJTLUmedia/markdown-formatter",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"markdown-mcp-server": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/XJTLUmedia/AI_answer_copier.git"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"markdown",
|
|
19
|
+
"converter",
|
|
20
|
+
"exporter",
|
|
21
|
+
"pdf",
|
|
22
|
+
"docx",
|
|
23
|
+
"latex",
|
|
24
|
+
"html",
|
|
25
|
+
"csv",
|
|
26
|
+
"json",
|
|
27
|
+
"xml",
|
|
28
|
+
"xlsx",
|
|
29
|
+
"rtf"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"start": "node dist/index.js",
|
|
34
|
+
"dev": "tsx src/index.ts",
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"inspector": "npx @modelcontextprotocol/inspector"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
41
|
+
"@types/node": "^20.11.0",
|
|
42
|
+
"docx": "^9.5.1",
|
|
43
|
+
"puppeteer": "^23.0.0",
|
|
44
|
+
"rehype-katex": "^7.0.0",
|
|
45
|
+
"rehype-parse": "^9.0.0",
|
|
46
|
+
"rehype-stringify": "^10.0.0",
|
|
47
|
+
"remark-gfm": "^4.0.1",
|
|
48
|
+
"remark-math": "^6.0.0",
|
|
49
|
+
"remark-parse": "^11.0.0",
|
|
50
|
+
"remark-rehype": "^11.1.0",
|
|
51
|
+
"remark-stringify": "^11.0.0",
|
|
52
|
+
"tsx": "^4.7.1",
|
|
53
|
+
"typescript": "^5.3.3",
|
|
54
|
+
"unified": "^11.0.5",
|
|
55
|
+
"xlsx": "^0.18.5",
|
|
56
|
+
"zod": "^3.23.8"
|
|
57
|
+
}
|
|
58
|
+
}
|