@neverprepared/mcp-markdown-to-confluence 1.1.4 → 1.2.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 (2) hide show
  1. package/dist/index.js +170 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4,7 +4,8 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
4
4
  import { z } from 'zod';
5
5
  import { ConfluenceClient } from 'confluence.js';
6
6
  import matter from 'gray-matter';
7
- import { readFile } from 'fs/promises';
7
+ import { readFile, readdir } from 'fs/promises';
8
+ import { join, extname, basename } from 'path';
8
9
  // Deep imports to avoid loading adaptors/filesystem.js which has broken CJS named exports.
9
10
  // Pin @markdown-confluence/lib version if these paths change.
10
11
  import { parseMarkdownToADF } from '@markdown-confluence/lib/dist/MdToADF.js';
@@ -95,6 +96,41 @@ function countDiagramBlocks(adf) {
95
96
  }
96
97
  return count;
97
98
  }
99
+ function pLimit(concurrency) {
100
+ let active = 0;
101
+ const queue = [];
102
+ function next() {
103
+ if (queue.length > 0 && active < concurrency) {
104
+ active++;
105
+ queue.shift()();
106
+ }
107
+ }
108
+ return (fn) => new Promise((resolve, reject) => {
109
+ queue.push(() => {
110
+ fn().then(resolve, reject).finally(() => {
111
+ active--;
112
+ next();
113
+ });
114
+ });
115
+ next();
116
+ });
117
+ }
118
+ async function parseMarkdownFile(filePath) {
119
+ const raw = await readFile(filePath, 'utf-8');
120
+ const parsed = matter(raw);
121
+ const title = parsed.data['connie-title'] ?? parsed.data['title'] ?? '';
122
+ const spaceKey = parsed.data['connie-space-key'] ?? '';
123
+ const pageId = parsed.data['connie-page-id']
124
+ ? String(parsed.data['connie-page-id'])
125
+ : undefined;
126
+ if (!title) {
127
+ return { skipped: true, filePath, reason: 'Missing "connie-title" or "title" in frontmatter' };
128
+ }
129
+ if (!spaceKey) {
130
+ return { skipped: true, filePath, reason: 'Missing "connie-space-key" in frontmatter' };
131
+ }
132
+ return { filePath, title, spaceKey, pageId, content: parsed.content };
133
+ }
98
134
  // ---------------------------------------------------------------------------
99
135
  // Core publish logic
100
136
  // ---------------------------------------------------------------------------
@@ -253,6 +289,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
253
289
  required: ['filePath'],
254
290
  },
255
291
  },
292
+ {
293
+ name: 'markdown_publish_directory',
294
+ description: 'Scan a directory for markdown files with Confluence frontmatter and publish them all concurrently. ' +
295
+ 'Files without required frontmatter (connie-title, connie-space-key) are skipped.',
296
+ inputSchema: {
297
+ type: 'object',
298
+ properties: {
299
+ directoryPath: {
300
+ type: 'string',
301
+ description: 'Absolute path to the directory containing markdown files',
302
+ },
303
+ skip_preview: {
304
+ type: 'boolean',
305
+ description: 'Set to true to skip preview and publish immediately',
306
+ default: false,
307
+ },
308
+ concurrency: {
309
+ type: 'number',
310
+ description: 'Maximum number of files to publish concurrently (default: 5)',
311
+ default: 5,
312
+ },
313
+ },
314
+ required: ['directoryPath'],
315
+ },
316
+ },
256
317
  ],
257
318
  }));
258
319
  // Tool handlers
@@ -373,6 +434,114 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
373
434
  ],
374
435
  };
375
436
  }
437
+ if (name === 'markdown_publish_directory') {
438
+ const input = z
439
+ .object({
440
+ directoryPath: z.string(),
441
+ skip_preview: z.boolean().default(false),
442
+ concurrency: z.number().int().min(1).max(20).default(5),
443
+ })
444
+ .parse(args);
445
+ const entries = await readdir(input.directoryPath);
446
+ const mdFiles = entries
447
+ .filter((f) => extname(f).toLowerCase() === '.md')
448
+ .map((f) => join(input.directoryPath, f));
449
+ if (mdFiles.length === 0) {
450
+ return {
451
+ content: [{ type: 'text', text: `No .md files found in ${input.directoryPath}` }],
452
+ };
453
+ }
454
+ const parseResults = await Promise.all(mdFiles.map(parseMarkdownFile));
455
+ const valid = [];
456
+ const skipped = [];
457
+ for (const r of parseResults) {
458
+ if ('skipped' in r) {
459
+ skipped.push(r);
460
+ }
461
+ else {
462
+ valid.push(r);
463
+ }
464
+ }
465
+ if (!input.skip_preview) {
466
+ const lines = [
467
+ `=== DIRECTORY PREVIEW ===`,
468
+ `Directory: ${input.directoryPath}`,
469
+ `Total .md files: ${mdFiles.length}`,
470
+ `Files to publish: ${valid.length}`,
471
+ `Files skipped: ${skipped.length}`,
472
+ '',
473
+ '--- Files to publish ---',
474
+ ];
475
+ for (const f of valid) {
476
+ const adf = parseMarkdownToADF(f.content, CONFLUENCE_BASE_URL);
477
+ const diagrams = countDiagramBlocks(adf);
478
+ lines.push(` ${basename(f.filePath)}`);
479
+ lines.push(` Title: ${f.title} | Space: ${f.spaceKey}` +
480
+ (f.pageId ? ` | Page ID: ${f.pageId}` : ' (new page)') +
481
+ (diagrams > 0 ? ` | Diagrams: ${diagrams}` : ''));
482
+ }
483
+ if (skipped.length > 0) {
484
+ lines.push('', '--- Skipped files ---');
485
+ for (const s of skipped) {
486
+ lines.push(` ${basename(s.filePath)}: ${s.reason}`);
487
+ }
488
+ }
489
+ lines.push('', `Call again with skip_preview: true to publish all ${valid.length} file(s).`);
490
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
491
+ }
492
+ const limit = pLimit(input.concurrency);
493
+ const results = await Promise.all(valid.map((f) => limit(async () => {
494
+ try {
495
+ const result = await publishMarkdown(f.content, f.title, f.spaceKey, f.pageId, undefined, true);
496
+ return {
497
+ filePath: f.filePath,
498
+ title: f.title,
499
+ success: true,
500
+ pageId: result.pageId,
501
+ version: result.version,
502
+ diagramCount: result.diagramCount,
503
+ url: result.url,
504
+ };
505
+ }
506
+ catch (err) {
507
+ return {
508
+ filePath: f.filePath,
509
+ title: f.title,
510
+ success: false,
511
+ error: err instanceof Error ? err.message : String(err),
512
+ };
513
+ }
514
+ })));
515
+ const succeeded = results.filter((r) => r.success);
516
+ const failed = results.filter((r) => !r.success);
517
+ const lines = [
518
+ `=== DIRECTORY PUBLISH RESULTS ===`,
519
+ `Directory: ${input.directoryPath}`,
520
+ `Succeeded: ${succeeded.length} | Failed: ${failed.length} | Skipped: ${skipped.length}`,
521
+ '',
522
+ ];
523
+ if (succeeded.length > 0) {
524
+ lines.push('--- Succeeded ---');
525
+ for (const r of succeeded) {
526
+ lines.push(` "${r.title}"`);
527
+ lines.push(` Page ID: ${r.pageId} | Version: ${r.version} | Diagrams: ${r.diagramCount} | URL: ${r.url}`);
528
+ }
529
+ }
530
+ if (failed.length > 0) {
531
+ lines.push('', '--- Failed ---');
532
+ for (const r of failed) {
533
+ lines.push(` "${r.title}" (${basename(r.filePath)})`);
534
+ lines.push(` Error: ${r.error}`);
535
+ }
536
+ }
537
+ if (skipped.length > 0) {
538
+ lines.push('', '--- Skipped (invalid frontmatter) ---');
539
+ for (const s of skipped) {
540
+ lines.push(` ${basename(s.filePath)}: ${s.reason}`);
541
+ }
542
+ }
543
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
544
+ }
376
545
  return {
377
546
  isError: true,
378
547
  content: [{ type: 'text', text: `Error: Unknown tool "${name}"` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neverprepared/mcp-markdown-to-confluence",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server for converting markdown to Confluence ADF and publishing pages with diagram support via Kroki",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",