@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.
@@ -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
+ }