@neverprepared/mcp-markdown-to-confluence 1.1.0 → 1.1.4
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/bin/mcp-markdown-to-confluence.js +10 -0
- package/dist/index.js +29 -7
- package/dist/kroki/KrokiDiagramPlugin.js +6 -5
- package/dist/loader.js +57 -0
- package/package.json +7 -6
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { register } from 'node:module';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { dirname, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
register(pathToFileURL(resolve(__dirname, '..', 'dist', 'loader.js')));
|
|
9
|
+
|
|
10
|
+
await import(pathToFileURL(resolve(__dirname, '..', 'dist', 'index.js')));
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,12 @@ import { z } from 'zod';
|
|
|
5
5
|
import { ConfluenceClient } from 'confluence.js';
|
|
6
6
|
import matter from 'gray-matter';
|
|
7
7
|
import { readFile } from 'fs/promises';
|
|
8
|
-
|
|
8
|
+
// Deep imports to avoid loading adaptors/filesystem.js which has broken CJS named exports.
|
|
9
|
+
// Pin @markdown-confluence/lib version if these paths change.
|
|
10
|
+
import { parseMarkdownToADF } from '@markdown-confluence/lib/dist/MdToADF.js';
|
|
11
|
+
import { renderADFDoc } from '@markdown-confluence/lib/dist/ADFToMarkdown.js';
|
|
12
|
+
import { executeADFProcessingPipeline, createPublisherFunctions, } from '@markdown-confluence/lib/dist/ADFProcessingPlugins/types.js';
|
|
13
|
+
import { MermaidRendererPlugin } from '@markdown-confluence/lib/dist/ADFProcessingPlugins/MermaidRendererPlugin.js';
|
|
9
14
|
import { KrokiClient, KrokiMermaidRenderer, KrokiDiagramPlugin } from './kroki/index.js';
|
|
10
15
|
// ---------------------------------------------------------------------------
|
|
11
16
|
// Environment
|
|
@@ -18,10 +23,29 @@ const KROKI_URL = process.env.KROKI_URL ?? 'http://localhost:8371';
|
|
|
18
23
|
// Kroki client
|
|
19
24
|
// ---------------------------------------------------------------------------
|
|
20
25
|
const krokiClient = new KrokiClient(KROKI_URL);
|
|
26
|
+
// Diagram types and their preferred output format
|
|
27
|
+
// PNG is preferred when supported; SVG is the universal fallback
|
|
28
|
+
const KROKI_DIAGRAM_CONFIGS = [
|
|
29
|
+
{ type: 'plantuml', format: 'png' },
|
|
30
|
+
{ type: 'graphviz', format: 'png' },
|
|
31
|
+
{ type: 'dot', format: 'png' },
|
|
32
|
+
{ type: 'ditaa', format: 'svg' },
|
|
33
|
+
{ type: 'nomnoml', format: 'svg' },
|
|
34
|
+
{ type: 'd2', format: 'svg' },
|
|
35
|
+
{ type: 'dbml', format: 'svg' },
|
|
36
|
+
{ type: 'erd', format: 'svg' },
|
|
37
|
+
{ type: 'svgbob', format: 'svg' },
|
|
38
|
+
{ type: 'pikchr', format: 'svg' },
|
|
39
|
+
{ type: 'bytefield', format: 'svg' },
|
|
40
|
+
{ type: 'wavedrom', format: 'svg' },
|
|
41
|
+
{ type: 'vega', format: 'svg' },
|
|
42
|
+
{ type: 'vega-lite', format: 'svg' },
|
|
43
|
+
{ type: 'bpmn', format: 'svg' },
|
|
44
|
+
{ type: 'c4plantuml', format: 'png' },
|
|
45
|
+
];
|
|
21
46
|
const SUPPORTED_DIAGRAM_TYPES = [
|
|
22
|
-
'mermaid',
|
|
23
|
-
|
|
24
|
-
'excalidraw', 'bpmn', 'erd', 'dbml', 'c4plantuml',
|
|
47
|
+
'mermaid',
|
|
48
|
+
...KROKI_DIAGRAM_CONFIGS.map((c) => c.type),
|
|
25
49
|
];
|
|
26
50
|
// ---------------------------------------------------------------------------
|
|
27
51
|
// Confluence client
|
|
@@ -140,9 +164,7 @@ async function publishMarkdown(markdown, title, spaceKey, pageId, parentId, skip
|
|
|
140
164
|
// Run ADF processing pipeline (renders diagrams via Kroki)
|
|
141
165
|
const finalAdf = await executeADFProcessingPipeline([
|
|
142
166
|
new MermaidRendererPlugin(new KrokiMermaidRenderer(krokiClient)),
|
|
143
|
-
...
|
|
144
|
-
.filter((t) => t !== 'mermaid')
|
|
145
|
-
.map((t) => new KrokiDiagramPlugin(t, krokiClient)),
|
|
167
|
+
...KROKI_DIAGRAM_CONFIGS.map((c) => new KrokiDiagramPlugin(c.type, krokiClient, c.format)),
|
|
146
168
|
], adf, publisherFunctions);
|
|
147
169
|
// Update the page with the final ADF
|
|
148
170
|
const updateParams = {
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { filter, traverse } from '@atlaskit/adf-utils/traverse';
|
|
2
2
|
import SparkMD5 from 'spark-md5';
|
|
3
|
-
function getDiagramFileName(diagramType, content) {
|
|
3
|
+
function getDiagramFileName(diagramType, content, outputFormat) {
|
|
4
4
|
const text = content ?? `${diagramType} placeholder`;
|
|
5
5
|
const hash = SparkMD5.hash(text);
|
|
6
|
-
const
|
|
6
|
+
const ext = outputFormat === 'png' ? 'png' : 'svg';
|
|
7
|
+
const uploadFilename = `RenderedKrokiChart-${diagramType}-${hash}.${ext}`;
|
|
7
8
|
return { uploadFilename, text };
|
|
8
9
|
}
|
|
9
10
|
export class KrokiDiagramPlugin {
|
|
10
11
|
diagramType;
|
|
11
12
|
client;
|
|
12
13
|
outputFormat;
|
|
13
|
-
constructor(diagramType, client, outputFormat = '
|
|
14
|
+
constructor(diagramType, client, outputFormat = 'svg') {
|
|
14
15
|
this.diagramType = diagramType;
|
|
15
16
|
this.client = client;
|
|
16
17
|
this.outputFormat = outputFormat;
|
|
@@ -19,7 +20,7 @@ export class KrokiDiagramPlugin {
|
|
|
19
20
|
const nodes = filter(adf, (node) => node.type === 'codeBlock' &&
|
|
20
21
|
(node.attrs || {})?.['language'] === this.diagramType);
|
|
21
22
|
const charts = new Set(nodes.map((node) => {
|
|
22
|
-
const details = getDiagramFileName(this.diagramType, node?.content?.at(0)?.text);
|
|
23
|
+
const details = getDiagramFileName(this.diagramType, node?.content?.at(0)?.text, this.outputFormat);
|
|
23
24
|
return {
|
|
24
25
|
name: details.uploadFilename,
|
|
25
26
|
data: details.text,
|
|
@@ -52,7 +53,7 @@ export class KrokiDiagramPlugin {
|
|
|
52
53
|
if (!content) {
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
|
-
const filename = getDiagramFileName(this.diagramType, content);
|
|
56
|
+
const filename = getDiagramFileName(this.diagramType, content, this.outputFormat);
|
|
56
57
|
if (!imageMap[filename.uploadFilename]) {
|
|
57
58
|
return;
|
|
58
59
|
}
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
2
|
+
import { resolve as pathResolve, dirname } from 'node:path';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
function tryResolveFile(filePath) {
|
|
6
|
+
// Direct .js extension
|
|
7
|
+
if (existsSync(filePath + '.js')) {
|
|
8
|
+
return pathToFileURL(filePath + '.js').href;
|
|
9
|
+
}
|
|
10
|
+
// Directory with index.js
|
|
11
|
+
if (existsSync(filePath + '/index.js')) {
|
|
12
|
+
return pathToFileURL(filePath + '/index.js').href;
|
|
13
|
+
}
|
|
14
|
+
// Directory with package.json (module or main field)
|
|
15
|
+
if (existsSync(filePath + '/package.json')) {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(filePath + '/package.json', 'utf-8'));
|
|
18
|
+
const entry = pkg.module || pkg.main;
|
|
19
|
+
if (entry) {
|
|
20
|
+
const resolved = pathResolve(filePath, entry);
|
|
21
|
+
if (existsSync(resolved)) {
|
|
22
|
+
return pathToFileURL(resolved).href;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function load(url, context, nextLoad) {
|
|
31
|
+
if (url.endsWith('.json')) {
|
|
32
|
+
return nextLoad(url, { ...context, importAttributes: { type: 'json' } });
|
|
33
|
+
}
|
|
34
|
+
return nextLoad(url, context);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
38
|
+
try {
|
|
39
|
+
return await nextResolve(specifier, context);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err?.code === 'ERR_MODULE_NOT_FOUND' || err?.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
|
|
42
|
+
// Try from the error's resolved URL
|
|
43
|
+
if (err.url) {
|
|
44
|
+
const resolved = tryResolveFile(fileURLToPath(err.url));
|
|
45
|
+
if (resolved) return { url: resolved, shortCircuit: true };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try from parent for relative specifiers
|
|
49
|
+
if (specifier.startsWith('.') && context.parentURL) {
|
|
50
|
+
const parentPath = dirname(fileURLToPath(context.parentURL));
|
|
51
|
+
const resolved = tryResolveFile(pathResolve(parentPath, specifier));
|
|
52
|
+
if (resolved) return { url: resolved, shortCircuit: true };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neverprepared/mcp-markdown-to-confluence",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"mcp-markdown-to-confluence": "
|
|
8
|
+
"mcp-markdown-to-confluence": "bin/mcp-markdown-to-confluence.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
+
"bin",
|
|
11
12
|
"dist",
|
|
12
13
|
"docker",
|
|
13
14
|
"scripts"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
|
-
"build": "tsc",
|
|
17
|
-
"start": "node
|
|
18
|
-
"dev": "node --watch
|
|
17
|
+
"build": "tsc && cp src/loader.js dist/loader.js",
|
|
18
|
+
"start": "node bin/mcp-markdown-to-confluence.js",
|
|
19
|
+
"dev": "node --watch bin/mcp-markdown-to-confluence.js",
|
|
19
20
|
"postinstall": "sh scripts/postinstall.sh"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@markdown-confluence/lib": "
|
|
23
|
+
"@markdown-confluence/lib": "5.5.2",
|
|
23
24
|
"@modelcontextprotocol/sdk": "^1.10.1",
|
|
24
25
|
"confluence.js": "^1.6.3",
|
|
25
26
|
"gray-matter": "^4.0.3",
|