@neverprepared/mcp-markdown-to-confluence 1.0.1
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/.github/workflows/ci.yml +28 -0
- package/.github/workflows/release-please.yml +48 -0
- package/CHANGELOG.md +16 -0
- package/README.md +134 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +357 -0
- package/package.json +47 -0
- package/src/index.ts +482 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ci:
|
|
11
|
+
name: Build
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 20
|
|
20
|
+
cache: npm
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
env:
|
|
25
|
+
PUPPETEER_SKIP_DOWNLOAD: "true"
|
|
26
|
+
|
|
27
|
+
- name: Build
|
|
28
|
+
run: npm run build
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Release Please
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
pull-requests: write
|
|
10
|
+
packages: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
release-please:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
outputs:
|
|
16
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
17
|
+
steps:
|
|
18
|
+
- uses: googleapis/release-please-action@v4
|
|
19
|
+
id: release
|
|
20
|
+
with:
|
|
21
|
+
release-type: node
|
|
22
|
+
|
|
23
|
+
publish:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
needs: release-please
|
|
26
|
+
if: needs.release-please.outputs.release_created == 'true'
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- uses: actions/setup-node@v4
|
|
32
|
+
with:
|
|
33
|
+
node-version: 20
|
|
34
|
+
cache: npm
|
|
35
|
+
registry-url: https://registry.npmjs.org/
|
|
36
|
+
|
|
37
|
+
- name: Install dependencies
|
|
38
|
+
run: npm ci
|
|
39
|
+
env:
|
|
40
|
+
PUPPETEER_SKIP_DOWNLOAD: "true"
|
|
41
|
+
|
|
42
|
+
- name: Build
|
|
43
|
+
run: npm run build
|
|
44
|
+
|
|
45
|
+
- name: Publish to npm
|
|
46
|
+
run: npm publish --access public
|
|
47
|
+
env:
|
|
48
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.1](https://github.com/neverprepared/mcp-markdown-to-confluence/compare/v1.0.0...v1.0.1) (2026-04-01)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* resolve packages from npm registry and fix API compatibility ([7b36bd7](https://github.com/neverprepared/mcp-markdown-to-confluence/commit/7b36bd78f9f0ad07c2e0e60658584b443215668e))
|
|
9
|
+
* skip Puppeteer Chromium download in CI ([31da53d](https://github.com/neverprepared/mcp-markdown-to-confluence/commit/31da53d48ea88b5444df5715bf414a4d4d70cca5))
|
|
10
|
+
|
|
11
|
+
## 1.0.0 (2026-04-01)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* resolve packages from npm registry and fix API compatibility ([7b36bd7](https://github.com/neverprepared/mcp-markdown-to-confluence/commit/7b36bd78f9f0ad07c2e0e60658584b443215668e))
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# mcp-markdown-to-confluence
|
|
2
|
+
|
|
3
|
+
An MCP (Model Context Protocol) server that converts Markdown files to Atlassian Document Format (ADF) and publishes them to Confluence. Mermaid diagrams are rendered to PNG images and uploaded as page attachments.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Convert Markdown → Confluence ADF with full formatting support (tables, code blocks, callouts, TOC)
|
|
8
|
+
- Render Mermaid diagrams to PNG via headless Chromium and attach them to pages
|
|
9
|
+
- Preview content before publishing
|
|
10
|
+
- Create new pages or update existing ones
|
|
11
|
+
- Publish from inline Markdown or directly from a `.md` file using frontmatter
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### npm
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @neverprepared/mcp-markdown-to-confluence
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### From source
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/neverprepared/mcp-markdown-to-confluence.git
|
|
25
|
+
cd mcp-markdown-to-confluence
|
|
26
|
+
npm install
|
|
27
|
+
npm run build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Set the following environment variables:
|
|
33
|
+
|
|
34
|
+
| Variable | Description |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `CONFLUENCE_BASE_URL` | Your Confluence base URL, e.g. `https://your-org.atlassian.net` |
|
|
37
|
+
| `CONFLUENCE_USERNAME` | Your Atlassian account email |
|
|
38
|
+
| `CONFLUENCE_API_TOKEN` | Your Atlassian API token ([create one here](https://id.atlassian.com/manage-profile/security/api-tokens)) |
|
|
39
|
+
|
|
40
|
+
## Claude Code Setup
|
|
41
|
+
|
|
42
|
+
Add to your Claude Code MCP config (`.claude/.claude.json`):
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"markdown-to-confluence": {
|
|
48
|
+
"command": "node",
|
|
49
|
+
"args": ["/path/to/mcp-markdown-to-confluence/dist/index.js"],
|
|
50
|
+
"env": {
|
|
51
|
+
"CONFLUENCE_BASE_URL": "https://your-org.atlassian.net",
|
|
52
|
+
"CONFLUENCE_USERNAME": "you@example.com",
|
|
53
|
+
"CONFLUENCE_API_TOKEN": "your-api-token"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Tools
|
|
61
|
+
|
|
62
|
+
### `markdown_preview`
|
|
63
|
+
|
|
64
|
+
Convert Markdown to ADF and return a text preview — no Confluence calls made.
|
|
65
|
+
|
|
66
|
+
| Parameter | Type | Required | Description |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| `markdown` | string | yes | Markdown content to preview |
|
|
69
|
+
| `title` | string | yes | Page title |
|
|
70
|
+
|
|
71
|
+
### `markdown_publish`
|
|
72
|
+
|
|
73
|
+
Publish Markdown to a Confluence page. By default shows a preview first.
|
|
74
|
+
|
|
75
|
+
| Parameter | Type | Required | Description |
|
|
76
|
+
|---|---|---|---|
|
|
77
|
+
| `markdown` | string | yes | Markdown content |
|
|
78
|
+
| `title` | string | yes | Confluence page title |
|
|
79
|
+
| `spaceKey` | string | yes | Confluence space key (e.g. `ENG`) |
|
|
80
|
+
| `pageId` | string | no | Existing page ID to update; omit to create a new page |
|
|
81
|
+
| `parentId` | string | no | Parent page ID for new pages |
|
|
82
|
+
| `skip_preview` | boolean | no | Set `true` to publish without preview (default: `false`) |
|
|
83
|
+
|
|
84
|
+
### `markdown_publish_file`
|
|
85
|
+
|
|
86
|
+
Read a `.md` file from disk and publish it. Page metadata is read from frontmatter.
|
|
87
|
+
|
|
88
|
+
| Parameter | Type | Required | Description |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| `filePath` | string | yes | Absolute path to the Markdown file |
|
|
91
|
+
| `skip_preview` | boolean | no | Set `true` to publish without preview (default: `false`) |
|
|
92
|
+
|
|
93
|
+
**Supported frontmatter keys:**
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
---
|
|
97
|
+
connie-title: My Page Title
|
|
98
|
+
connie-space-key: ENG
|
|
99
|
+
connie-page-id: "123456" # omit to create a new page
|
|
100
|
+
title: Fallback title # used if connie-title is absent
|
|
101
|
+
---
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Preview Flow
|
|
105
|
+
|
|
106
|
+
By default, `markdown_publish` and `markdown_publish_file` return a rendered preview and prompt you to confirm before publishing. To publish in one step, pass `skip_preview: true`.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
# Step 1 — review
|
|
110
|
+
markdown_publish(markdown: "...", title: "My Page", spaceKey: "ENG")
|
|
111
|
+
→ returns preview text
|
|
112
|
+
|
|
113
|
+
# Step 2 — publish
|
|
114
|
+
markdown_publish(markdown: "...", title: "My Page", spaceKey: "ENG", skip_preview: true)
|
|
115
|
+
→ returns page URL
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Mermaid Diagrams
|
|
119
|
+
|
|
120
|
+
Mermaid code blocks are automatically detected, rendered to PNG via headless Chromium (bundled with Puppeteer), and uploaded as Confluence page attachments. The first run will download Chromium (~170 MB).
|
|
121
|
+
|
|
122
|
+
````markdown
|
|
123
|
+
```mermaid
|
|
124
|
+
flowchart TD
|
|
125
|
+
A[Write Markdown] --> B[Preview]
|
|
126
|
+
B --> C{Looks good?}
|
|
127
|
+
C -- Yes --> D[Publish]
|
|
128
|
+
C -- No --> A
|
|
129
|
+
```
|
|
130
|
+
````
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { ConfluenceClient } from 'confluence.js';
|
|
6
|
+
import matter from 'gray-matter';
|
|
7
|
+
import { readFile } from 'fs/promises';
|
|
8
|
+
import { parseMarkdownToADF, renderADFDoc, executeADFProcessingPipeline, createPublisherFunctions, } from '@markdown-confluence/lib';
|
|
9
|
+
import { MermaidRendererPlugin } from '@markdown-confluence/lib';
|
|
10
|
+
import { PuppeteerMermaidRenderer } from '@markdown-confluence/mermaid-puppeteer-renderer';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Environment
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
const CONFLUENCE_BASE_URL = process.env.CONFLUENCE_BASE_URL ?? '';
|
|
15
|
+
const CONFLUENCE_USERNAME = process.env.CONFLUENCE_USERNAME ?? '';
|
|
16
|
+
const CONFLUENCE_API_TOKEN = process.env.CONFLUENCE_API_TOKEN ?? '';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Confluence client
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const confluenceClient = new ConfluenceClient({
|
|
21
|
+
host: CONFLUENCE_BASE_URL,
|
|
22
|
+
authentication: {
|
|
23
|
+
basic: {
|
|
24
|
+
email: CONFLUENCE_USERNAME,
|
|
25
|
+
apiToken: CONFLUENCE_API_TOKEN,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Stub LoaderAdaptor — only uploadBuffer is called by the mermaid plugin
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
const stubAdaptor = {
|
|
33
|
+
readFile: async (_filePath) => undefined,
|
|
34
|
+
readBinary: async (_filePath) => false,
|
|
35
|
+
fileExists: async (_filePath) => false,
|
|
36
|
+
listFiles: async () => [],
|
|
37
|
+
uploadBuffer: async (_buffer, _fileName, _mimeType) => undefined,
|
|
38
|
+
};
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Helpers
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
function countMermaidBlocks(adf) {
|
|
43
|
+
if (typeof adf !== 'object' || adf === null)
|
|
44
|
+
return 0;
|
|
45
|
+
const node = adf;
|
|
46
|
+
let count = 0;
|
|
47
|
+
if (node['type'] === 'codeBlock' &&
|
|
48
|
+
typeof node['attrs'] === 'object' &&
|
|
49
|
+
node['attrs'] !== null &&
|
|
50
|
+
node['attrs']['language'] === 'mermaid') {
|
|
51
|
+
count += 1;
|
|
52
|
+
}
|
|
53
|
+
for (const value of Object.values(node)) {
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
for (const item of value) {
|
|
56
|
+
count += countMermaidBlocks(item);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (typeof value === 'object' && value !== null) {
|
|
60
|
+
count += countMermaidBlocks(value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Core publish logic
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
async function publishMarkdown(markdown, title, spaceKey, pageId, parentId, skipPreview = false) {
|
|
69
|
+
// Parse markdown → ADF
|
|
70
|
+
const adf = parseMarkdownToADF(markdown, CONFLUENCE_BASE_URL);
|
|
71
|
+
const mermaidCount = countMermaidBlocks(adf);
|
|
72
|
+
if (!skipPreview) {
|
|
73
|
+
const previewText = renderADFDoc(adf);
|
|
74
|
+
return { isPreview: true, previewText, mermaidCount };
|
|
75
|
+
}
|
|
76
|
+
// ----- Full publish -----
|
|
77
|
+
let currentVersion = 1;
|
|
78
|
+
let resolvedPageId = pageId;
|
|
79
|
+
if (resolvedPageId) {
|
|
80
|
+
// Fetch existing page to get current version
|
|
81
|
+
const existingPage = await confluenceClient.content.getContentById({
|
|
82
|
+
id: resolvedPageId,
|
|
83
|
+
expand: ['version'],
|
|
84
|
+
});
|
|
85
|
+
currentVersion = existingPage.version.number;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// Create a placeholder page to obtain a pageId
|
|
89
|
+
const blankAdf = {
|
|
90
|
+
version: 1,
|
|
91
|
+
type: 'doc',
|
|
92
|
+
content: [],
|
|
93
|
+
};
|
|
94
|
+
const createParams = {
|
|
95
|
+
space: { key: spaceKey },
|
|
96
|
+
title,
|
|
97
|
+
type: 'page',
|
|
98
|
+
body: {
|
|
99
|
+
atlas_doc_format: {
|
|
100
|
+
value: JSON.stringify(blankAdf),
|
|
101
|
+
representation: 'atlas_doc_format',
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
if (parentId) {
|
|
106
|
+
createParams.ancestors = [{ id: parentId }];
|
|
107
|
+
}
|
|
108
|
+
const created = await confluenceClient.content.createContent(createParams);
|
|
109
|
+
resolvedPageId = created.id;
|
|
110
|
+
currentVersion = created.version.number;
|
|
111
|
+
}
|
|
112
|
+
// Fetch current attachments to build the map
|
|
113
|
+
const attachmentsResult = await confluenceClient.contentAttachments.getAttachments({
|
|
114
|
+
id: resolvedPageId,
|
|
115
|
+
});
|
|
116
|
+
const currentAttachments = {};
|
|
117
|
+
for (const att of attachmentsResult.results ?? []) {
|
|
118
|
+
const attTitle = att.title ?? '';
|
|
119
|
+
const fileId = att.extensions?.fileId ?? '';
|
|
120
|
+
const collectionName = att.extensions?.collectionName ?? '';
|
|
121
|
+
if (attTitle) {
|
|
122
|
+
currentAttachments[attTitle] = {
|
|
123
|
+
filehash: att.metadata?.comment ?? '',
|
|
124
|
+
attachmentId: fileId,
|
|
125
|
+
collectionName,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Build publisher functions
|
|
130
|
+
const publisherFunctions = createPublisherFunctions(confluenceClient, stubAdaptor, resolvedPageId, title, currentAttachments);
|
|
131
|
+
// Run ADF processing pipeline (renders mermaid diagrams)
|
|
132
|
+
const finalAdf = await executeADFProcessingPipeline([new MermaidRendererPlugin(new PuppeteerMermaidRenderer())], adf, publisherFunctions);
|
|
133
|
+
// Update the page with the final ADF
|
|
134
|
+
const updateParams = {
|
|
135
|
+
id: resolvedPageId,
|
|
136
|
+
title,
|
|
137
|
+
type: 'page',
|
|
138
|
+
version: { number: currentVersion + 1 },
|
|
139
|
+
body: {
|
|
140
|
+
atlas_doc_format: {
|
|
141
|
+
value: JSON.stringify(finalAdf),
|
|
142
|
+
representation: 'atlas_doc_format',
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
if (parentId) {
|
|
147
|
+
updateParams.ancestors = [{ id: parentId }];
|
|
148
|
+
}
|
|
149
|
+
await confluenceClient.content.updateContent(updateParams);
|
|
150
|
+
const url = `${CONFLUENCE_BASE_URL}/wiki/spaces/${spaceKey}/pages/${resolvedPageId}`;
|
|
151
|
+
return {
|
|
152
|
+
isPreview: false,
|
|
153
|
+
pageId: resolvedPageId,
|
|
154
|
+
version: currentVersion + 1,
|
|
155
|
+
mermaidCount,
|
|
156
|
+
url,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// MCP Server
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
const server = new Server({ name: 'mcp-markdown-to-confluence', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
163
|
+
// Tool definitions
|
|
164
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
165
|
+
tools: [
|
|
166
|
+
{
|
|
167
|
+
name: 'markdown_preview',
|
|
168
|
+
description: 'Convert markdown to Confluence ADF and return a text preview. Does not publish to Confluence.',
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: {
|
|
172
|
+
markdown: { type: 'string', description: 'Markdown content to preview' },
|
|
173
|
+
title: { type: 'string', description: 'Page title (used during ADF conversion)' },
|
|
174
|
+
},
|
|
175
|
+
required: ['markdown', 'title'],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'markdown_publish',
|
|
180
|
+
description: 'Publish markdown to a Confluence page. By default runs a preview first; set skip_preview: true to publish immediately.',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
markdown: { type: 'string', description: 'Markdown content to publish' },
|
|
185
|
+
title: { type: 'string', description: 'Confluence page title' },
|
|
186
|
+
spaceKey: { type: 'string', description: 'Confluence space key (e.g. "ENG")' },
|
|
187
|
+
pageId: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Existing page ID to update (omit to create a new page)',
|
|
190
|
+
},
|
|
191
|
+
parentId: {
|
|
192
|
+
type: 'string',
|
|
193
|
+
description: 'Parent page ID for new page creation',
|
|
194
|
+
},
|
|
195
|
+
skip_preview: {
|
|
196
|
+
type: 'boolean',
|
|
197
|
+
description: 'Set to true to skip preview and publish immediately',
|
|
198
|
+
default: false,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
required: ['markdown', 'title', 'spaceKey'],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: 'markdown_publish_file',
|
|
206
|
+
description: 'Read a markdown file from disk and publish it to Confluence. Frontmatter keys: connie-title / title, connie-space-key, connie-page-id.',
|
|
207
|
+
inputSchema: {
|
|
208
|
+
type: 'object',
|
|
209
|
+
properties: {
|
|
210
|
+
filePath: { type: 'string', description: 'Absolute path to the markdown file' },
|
|
211
|
+
skip_preview: {
|
|
212
|
+
type: 'boolean',
|
|
213
|
+
description: 'Set to true to skip preview and publish immediately',
|
|
214
|
+
default: false,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
required: ['filePath'],
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
}));
|
|
222
|
+
// Tool handlers
|
|
223
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
224
|
+
const { name, arguments: args } = request.params;
|
|
225
|
+
try {
|
|
226
|
+
if (name === 'markdown_preview') {
|
|
227
|
+
const input = z
|
|
228
|
+
.object({ markdown: z.string(), title: z.string() })
|
|
229
|
+
.parse(args);
|
|
230
|
+
const adf = parseMarkdownToADF(input.markdown, CONFLUENCE_BASE_URL);
|
|
231
|
+
const mermaidCount = countMermaidBlocks(adf);
|
|
232
|
+
const previewText = renderADFDoc(adf);
|
|
233
|
+
const lines = [previewText];
|
|
234
|
+
if (mermaidCount > 0) {
|
|
235
|
+
lines.push(`\n[Note: ${mermaidCount} mermaid diagram(s) detected — they will be rendered as images when published.]`);
|
|
236
|
+
}
|
|
237
|
+
return { content: [{ type: 'text', text: lines.join('') }] };
|
|
238
|
+
}
|
|
239
|
+
if (name === 'markdown_publish') {
|
|
240
|
+
const input = z
|
|
241
|
+
.object({
|
|
242
|
+
markdown: z.string(),
|
|
243
|
+
title: z.string(),
|
|
244
|
+
spaceKey: z.string(),
|
|
245
|
+
pageId: z.string().optional(),
|
|
246
|
+
parentId: z.string().optional(),
|
|
247
|
+
skip_preview: z.boolean().default(false),
|
|
248
|
+
})
|
|
249
|
+
.parse(args);
|
|
250
|
+
const result = await publishMarkdown(input.markdown, input.title, input.spaceKey, input.pageId, input.parentId, input.skip_preview);
|
|
251
|
+
if (result.isPreview) {
|
|
252
|
+
const lines = ['=== PREVIEW ===\n', result.previewText ?? ''];
|
|
253
|
+
if ((result.mermaidCount ?? 0) > 0) {
|
|
254
|
+
lines.push(`\n[Note: ${result.mermaidCount} mermaid diagram(s) detected — they will be rendered when published.]`);
|
|
255
|
+
}
|
|
256
|
+
lines.push('\n\nCall again with skip_preview: true to publish to Confluence.');
|
|
257
|
+
return { content: [{ type: 'text', text: lines.join('') }] };
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
content: [
|
|
261
|
+
{
|
|
262
|
+
type: 'text',
|
|
263
|
+
text: [
|
|
264
|
+
`Successfully published to Confluence.`,
|
|
265
|
+
`Title: ${input.title}`,
|
|
266
|
+
`Page ID: ${result.pageId}`,
|
|
267
|
+
`Version: ${result.version}`,
|
|
268
|
+
`Mermaid diagrams rendered: ${result.mermaidCount}`,
|
|
269
|
+
`URL: ${result.url}`,
|
|
270
|
+
].join('\n'),
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (name === 'markdown_publish_file') {
|
|
276
|
+
const input = z
|
|
277
|
+
.object({
|
|
278
|
+
filePath: z.string(),
|
|
279
|
+
skip_preview: z.boolean().default(false),
|
|
280
|
+
})
|
|
281
|
+
.parse(args);
|
|
282
|
+
const raw = await readFile(input.filePath, 'utf-8');
|
|
283
|
+
const parsed = matter(raw);
|
|
284
|
+
const title = parsed.data['connie-title'] ?? parsed.data['title'] ?? '';
|
|
285
|
+
const spaceKey = parsed.data['connie-space-key'] ?? '';
|
|
286
|
+
const pageId = parsed.data['connie-page-id']
|
|
287
|
+
? String(parsed.data['connie-page-id'])
|
|
288
|
+
: undefined;
|
|
289
|
+
if (!title) {
|
|
290
|
+
return {
|
|
291
|
+
isError: true,
|
|
292
|
+
content: [
|
|
293
|
+
{
|
|
294
|
+
type: 'text',
|
|
295
|
+
text: 'Error: Missing page title. Set "connie-title" or "title" in frontmatter.',
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (!spaceKey) {
|
|
301
|
+
return {
|
|
302
|
+
isError: true,
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: 'text',
|
|
306
|
+
text: 'Error: Missing space key. Set "connie-space-key" in frontmatter.',
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
const result = await publishMarkdown(parsed.content, title, spaceKey, pageId, undefined, input.skip_preview);
|
|
312
|
+
if (result.isPreview) {
|
|
313
|
+
const lines = [
|
|
314
|
+
`File: ${input.filePath}\n`,
|
|
315
|
+
'=== PREVIEW ===\n',
|
|
316
|
+
result.previewText ?? '',
|
|
317
|
+
];
|
|
318
|
+
if ((result.mermaidCount ?? 0) > 0) {
|
|
319
|
+
lines.push(`\n[Note: ${result.mermaidCount} mermaid diagram(s) detected — they will be rendered when published.]`);
|
|
320
|
+
}
|
|
321
|
+
lines.push('\n\nCall again with skip_preview: true to publish to Confluence.');
|
|
322
|
+
return { content: [{ type: 'text', text: lines.join('') }] };
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
content: [
|
|
326
|
+
{
|
|
327
|
+
type: 'text',
|
|
328
|
+
text: [
|
|
329
|
+
`Successfully published "${title}" to Confluence.`,
|
|
330
|
+
`File: ${input.filePath}`,
|
|
331
|
+
`Page ID: ${result.pageId}`,
|
|
332
|
+
`Version: ${result.version}`,
|
|
333
|
+
`Mermaid diagrams rendered: ${result.mermaidCount}`,
|
|
334
|
+
`URL: ${result.url}`,
|
|
335
|
+
].join('\n'),
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
isError: true,
|
|
342
|
+
content: [{ type: 'text', text: `Error: Unknown tool "${name}"` }],
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
347
|
+
return {
|
|
348
|
+
isError: true,
|
|
349
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Start
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
const transport = new StdioServerTransport();
|
|
357
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neverprepared/mcp-markdown-to-confluence",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "MCP server for converting markdown to Confluence ADF and publishing pages with mermaid diagram support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-markdown-to-confluence": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "node --watch dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@markdown-confluence/lib": "^5.5.2",
|
|
17
|
+
"@markdown-confluence/mermaid-puppeteer-renderer": "^5.5.2",
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.10.1",
|
|
19
|
+
"confluence.js": "^1.6.3",
|
|
20
|
+
"gray-matter": "^4.0.3",
|
|
21
|
+
"zod": "^3.24.3"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"typescript": "^5.8.3"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20.0.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"registry": "https://registry.npmjs.org/",
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/neverprepared/mcp-markdown-to-confluence.git"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"mcp",
|
|
40
|
+
"confluence",
|
|
41
|
+
"markdown",
|
|
42
|
+
"atlassian",
|
|
43
|
+
"mermaid",
|
|
44
|
+
"model-context-protocol"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { ConfluenceClient } from 'confluence.js';
|
|
9
|
+
import matter from 'gray-matter';
|
|
10
|
+
import { readFile } from 'fs/promises';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
parseMarkdownToADF,
|
|
14
|
+
renderADFDoc,
|
|
15
|
+
executeADFProcessingPipeline,
|
|
16
|
+
createPublisherFunctions,
|
|
17
|
+
} from '@markdown-confluence/lib';
|
|
18
|
+
import { MermaidRendererPlugin } from '@markdown-confluence/lib';
|
|
19
|
+
import { PuppeteerMermaidRenderer } from '@markdown-confluence/mermaid-puppeteer-renderer';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Environment
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const CONFLUENCE_BASE_URL = process.env.CONFLUENCE_BASE_URL ?? '';
|
|
26
|
+
const CONFLUENCE_USERNAME = process.env.CONFLUENCE_USERNAME ?? '';
|
|
27
|
+
const CONFLUENCE_API_TOKEN = process.env.CONFLUENCE_API_TOKEN ?? '';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Confluence client
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const confluenceClient = new ConfluenceClient({
|
|
34
|
+
host: CONFLUENCE_BASE_URL,
|
|
35
|
+
authentication: {
|
|
36
|
+
basic: {
|
|
37
|
+
email: CONFLUENCE_USERNAME,
|
|
38
|
+
apiToken: CONFLUENCE_API_TOKEN,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Stub LoaderAdaptor — only uploadBuffer is called by the mermaid plugin
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
const stubAdaptor = {
|
|
48
|
+
readFile: async (_filePath: string) => undefined,
|
|
49
|
+
readBinary: async (_filePath: string) => false as const,
|
|
50
|
+
fileExists: async (_filePath: string) => false,
|
|
51
|
+
listFiles: async () => [],
|
|
52
|
+
uploadBuffer: async (
|
|
53
|
+
_buffer: Buffer,
|
|
54
|
+
_fileName: string,
|
|
55
|
+
_mimeType: string
|
|
56
|
+
) => undefined,
|
|
57
|
+
} as unknown as import('@markdown-confluence/lib').LoaderAdaptor;
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function countMermaidBlocks(adf: unknown): number {
|
|
64
|
+
if (typeof adf !== 'object' || adf === null) return 0;
|
|
65
|
+
|
|
66
|
+
const node = adf as Record<string, unknown>;
|
|
67
|
+
let count = 0;
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
node['type'] === 'codeBlock' &&
|
|
71
|
+
typeof node['attrs'] === 'object' &&
|
|
72
|
+
node['attrs'] !== null &&
|
|
73
|
+
(node['attrs'] as Record<string, unknown>)['language'] === 'mermaid'
|
|
74
|
+
) {
|
|
75
|
+
count += 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const value of Object.values(node)) {
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
for (const item of value) {
|
|
81
|
+
count += countMermaidBlocks(item);
|
|
82
|
+
}
|
|
83
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
84
|
+
count += countMermaidBlocks(value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Core publish logic
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
async function publishMarkdown(
|
|
96
|
+
markdown: string,
|
|
97
|
+
title: string,
|
|
98
|
+
spaceKey: string,
|
|
99
|
+
pageId?: string,
|
|
100
|
+
parentId?: string,
|
|
101
|
+
skipPreview = false
|
|
102
|
+
): Promise<{ isPreview: boolean; previewText?: string; mermaidCount?: number; pageId?: string; version?: number; url?: string }> {
|
|
103
|
+
// Parse markdown → ADF
|
|
104
|
+
const adf = parseMarkdownToADF(
|
|
105
|
+
markdown,
|
|
106
|
+
CONFLUENCE_BASE_URL
|
|
107
|
+
) as unknown as any;
|
|
108
|
+
|
|
109
|
+
const mermaidCount = countMermaidBlocks(adf);
|
|
110
|
+
|
|
111
|
+
if (!skipPreview) {
|
|
112
|
+
const previewText = renderADFDoc(adf as unknown as any);
|
|
113
|
+
return { isPreview: true, previewText, mermaidCount };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ----- Full publish -----
|
|
117
|
+
|
|
118
|
+
let currentVersion = 1;
|
|
119
|
+
let resolvedPageId = pageId;
|
|
120
|
+
|
|
121
|
+
if (resolvedPageId) {
|
|
122
|
+
// Fetch existing page to get current version
|
|
123
|
+
const existingPage = await confluenceClient.content.getContentById({
|
|
124
|
+
id: resolvedPageId,
|
|
125
|
+
expand: ['version'],
|
|
126
|
+
});
|
|
127
|
+
currentVersion = existingPage.version!.number!;
|
|
128
|
+
} else {
|
|
129
|
+
// Create a placeholder page to obtain a pageId
|
|
130
|
+
const blankAdf = {
|
|
131
|
+
version: 1,
|
|
132
|
+
type: 'doc',
|
|
133
|
+
content: [],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const createParams: Parameters<typeof confluenceClient.content.createContent>[0] = {
|
|
137
|
+
space: { key: spaceKey },
|
|
138
|
+
title,
|
|
139
|
+
type: 'page',
|
|
140
|
+
body: {
|
|
141
|
+
atlas_doc_format: {
|
|
142
|
+
value: JSON.stringify(blankAdf),
|
|
143
|
+
representation: 'atlas_doc_format',
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
if (parentId) {
|
|
149
|
+
createParams.ancestors = [{ id: parentId }];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const created = await confluenceClient.content.createContent(createParams);
|
|
153
|
+
resolvedPageId = created.id!;
|
|
154
|
+
currentVersion = created.version!.number!;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fetch current attachments to build the map
|
|
158
|
+
const attachmentsResult = await confluenceClient.contentAttachments.getAttachments({
|
|
159
|
+
id: resolvedPageId,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
type CurrentAttachments = Record<
|
|
163
|
+
string,
|
|
164
|
+
{ filehash: string; attachmentId: string; collectionName: string }
|
|
165
|
+
>;
|
|
166
|
+
|
|
167
|
+
const currentAttachments: CurrentAttachments = {};
|
|
168
|
+
for (const att of attachmentsResult.results ?? []) {
|
|
169
|
+
const attTitle = att.title ?? '';
|
|
170
|
+
const fileId = (att.extensions as any)?.fileId ?? '';
|
|
171
|
+
const collectionName = (att.extensions as any)?.collectionName ?? '';
|
|
172
|
+
if (attTitle) {
|
|
173
|
+
currentAttachments[attTitle] = {
|
|
174
|
+
filehash: (att.metadata as any)?.comment ?? '',
|
|
175
|
+
attachmentId: fileId,
|
|
176
|
+
collectionName,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Build publisher functions
|
|
182
|
+
const publisherFunctions = createPublisherFunctions(
|
|
183
|
+
confluenceClient as unknown as any,
|
|
184
|
+
stubAdaptor,
|
|
185
|
+
resolvedPageId,
|
|
186
|
+
title,
|
|
187
|
+
currentAttachments
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Run ADF processing pipeline (renders mermaid diagrams)
|
|
191
|
+
const finalAdf = await executeADFProcessingPipeline(
|
|
192
|
+
[new MermaidRendererPlugin(new PuppeteerMermaidRenderer())],
|
|
193
|
+
adf as unknown as any,
|
|
194
|
+
publisherFunctions
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Update the page with the final ADF
|
|
198
|
+
const updateParams: Parameters<typeof confluenceClient.content.updateContent>[0] = {
|
|
199
|
+
id: resolvedPageId,
|
|
200
|
+
title,
|
|
201
|
+
type: 'page',
|
|
202
|
+
version: { number: currentVersion + 1 },
|
|
203
|
+
body: {
|
|
204
|
+
atlas_doc_format: {
|
|
205
|
+
value: JSON.stringify(finalAdf),
|
|
206
|
+
representation: 'atlas_doc_format',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (parentId) {
|
|
212
|
+
updateParams.ancestors = [{ id: parentId }];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await confluenceClient.content.updateContent(updateParams);
|
|
216
|
+
|
|
217
|
+
const url = `${CONFLUENCE_BASE_URL}/wiki/spaces/${spaceKey}/pages/${resolvedPageId}`;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
isPreview: false,
|
|
221
|
+
pageId: resolvedPageId,
|
|
222
|
+
version: currentVersion + 1,
|
|
223
|
+
mermaidCount,
|
|
224
|
+
url,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// MCP Server
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
const server = new Server(
|
|
233
|
+
{ name: 'mcp-markdown-to-confluence', version: '1.0.0' },
|
|
234
|
+
{ capabilities: { tools: {} } }
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Tool definitions
|
|
238
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
239
|
+
tools: [
|
|
240
|
+
{
|
|
241
|
+
name: 'markdown_preview',
|
|
242
|
+
description:
|
|
243
|
+
'Convert markdown to Confluence ADF and return a text preview. Does not publish to Confluence.',
|
|
244
|
+
inputSchema: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
markdown: { type: 'string', description: 'Markdown content to preview' },
|
|
248
|
+
title: { type: 'string', description: 'Page title (used during ADF conversion)' },
|
|
249
|
+
},
|
|
250
|
+
required: ['markdown', 'title'],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'markdown_publish',
|
|
255
|
+
description:
|
|
256
|
+
'Publish markdown to a Confluence page. By default runs a preview first; set skip_preview: true to publish immediately.',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties: {
|
|
260
|
+
markdown: { type: 'string', description: 'Markdown content to publish' },
|
|
261
|
+
title: { type: 'string', description: 'Confluence page title' },
|
|
262
|
+
spaceKey: { type: 'string', description: 'Confluence space key (e.g. "ENG")' },
|
|
263
|
+
pageId: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Existing page ID to update (omit to create a new page)',
|
|
266
|
+
},
|
|
267
|
+
parentId: {
|
|
268
|
+
type: 'string',
|
|
269
|
+
description: 'Parent page ID for new page creation',
|
|
270
|
+
},
|
|
271
|
+
skip_preview: {
|
|
272
|
+
type: 'boolean',
|
|
273
|
+
description: 'Set to true to skip preview and publish immediately',
|
|
274
|
+
default: false,
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
required: ['markdown', 'title', 'spaceKey'],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'markdown_publish_file',
|
|
282
|
+
description:
|
|
283
|
+
'Read a markdown file from disk and publish it to Confluence. Frontmatter keys: connie-title / title, connie-space-key, connie-page-id.',
|
|
284
|
+
inputSchema: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
filePath: { type: 'string', description: 'Absolute path to the markdown file' },
|
|
288
|
+
skip_preview: {
|
|
289
|
+
type: 'boolean',
|
|
290
|
+
description: 'Set to true to skip preview and publish immediately',
|
|
291
|
+
default: false,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
required: ['filePath'],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
// Tool handlers
|
|
301
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
302
|
+
const { name, arguments: args } = request.params;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
if (name === 'markdown_preview') {
|
|
306
|
+
const input = z
|
|
307
|
+
.object({ markdown: z.string(), title: z.string() })
|
|
308
|
+
.parse(args);
|
|
309
|
+
|
|
310
|
+
const adf = parseMarkdownToADF(
|
|
311
|
+
input.markdown,
|
|
312
|
+
CONFLUENCE_BASE_URL
|
|
313
|
+
) as unknown as any;
|
|
314
|
+
|
|
315
|
+
const mermaidCount = countMermaidBlocks(adf);
|
|
316
|
+
const previewText = renderADFDoc(adf as unknown as any);
|
|
317
|
+
|
|
318
|
+
const lines: string[] = [previewText];
|
|
319
|
+
if (mermaidCount > 0) {
|
|
320
|
+
lines.push(
|
|
321
|
+
`\n[Note: ${mermaidCount} mermaid diagram(s) detected — they will be rendered as images when published.]`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { content: [{ type: 'text', text: lines.join('') }] };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (name === 'markdown_publish') {
|
|
329
|
+
const input = z
|
|
330
|
+
.object({
|
|
331
|
+
markdown: z.string(),
|
|
332
|
+
title: z.string(),
|
|
333
|
+
spaceKey: z.string(),
|
|
334
|
+
pageId: z.string().optional(),
|
|
335
|
+
parentId: z.string().optional(),
|
|
336
|
+
skip_preview: z.boolean().default(false),
|
|
337
|
+
})
|
|
338
|
+
.parse(args);
|
|
339
|
+
|
|
340
|
+
const result = await publishMarkdown(
|
|
341
|
+
input.markdown,
|
|
342
|
+
input.title,
|
|
343
|
+
input.spaceKey,
|
|
344
|
+
input.pageId,
|
|
345
|
+
input.parentId,
|
|
346
|
+
input.skip_preview
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
if (result.isPreview) {
|
|
350
|
+
const lines: string[] = ['=== PREVIEW ===\n', result.previewText ?? ''];
|
|
351
|
+
if ((result.mermaidCount ?? 0) > 0) {
|
|
352
|
+
lines.push(
|
|
353
|
+
`\n[Note: ${result.mermaidCount} mermaid diagram(s) detected — they will be rendered when published.]`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
lines.push(
|
|
357
|
+
'\n\nCall again with skip_preview: true to publish to Confluence.'
|
|
358
|
+
);
|
|
359
|
+
return { content: [{ type: 'text', text: lines.join('') }] };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
content: [
|
|
364
|
+
{
|
|
365
|
+
type: 'text',
|
|
366
|
+
text: [
|
|
367
|
+
`Successfully published to Confluence.`,
|
|
368
|
+
`Title: ${input.title}`,
|
|
369
|
+
`Page ID: ${result.pageId}`,
|
|
370
|
+
`Version: ${result.version}`,
|
|
371
|
+
`Mermaid diagrams rendered: ${result.mermaidCount}`,
|
|
372
|
+
`URL: ${result.url}`,
|
|
373
|
+
].join('\n'),
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (name === 'markdown_publish_file') {
|
|
380
|
+
const input = z
|
|
381
|
+
.object({
|
|
382
|
+
filePath: z.string(),
|
|
383
|
+
skip_preview: z.boolean().default(false),
|
|
384
|
+
})
|
|
385
|
+
.parse(args);
|
|
386
|
+
|
|
387
|
+
const raw = await readFile(input.filePath, 'utf-8');
|
|
388
|
+
const parsed = matter(raw);
|
|
389
|
+
|
|
390
|
+
const title: string =
|
|
391
|
+
parsed.data['connie-title'] ?? parsed.data['title'] ?? '';
|
|
392
|
+
const spaceKey: string = parsed.data['connie-space-key'] ?? '';
|
|
393
|
+
const pageId: string | undefined = parsed.data['connie-page-id']
|
|
394
|
+
? String(parsed.data['connie-page-id'])
|
|
395
|
+
: undefined;
|
|
396
|
+
|
|
397
|
+
if (!title) {
|
|
398
|
+
return {
|
|
399
|
+
isError: true,
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: 'text',
|
|
403
|
+
text: 'Error: Missing page title. Set "connie-title" or "title" in frontmatter.',
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!spaceKey) {
|
|
410
|
+
return {
|
|
411
|
+
isError: true,
|
|
412
|
+
content: [
|
|
413
|
+
{
|
|
414
|
+
type: 'text',
|
|
415
|
+
text: 'Error: Missing space key. Set "connie-space-key" in frontmatter.',
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const result = await publishMarkdown(
|
|
422
|
+
parsed.content,
|
|
423
|
+
title,
|
|
424
|
+
spaceKey,
|
|
425
|
+
pageId,
|
|
426
|
+
undefined,
|
|
427
|
+
input.skip_preview
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
if (result.isPreview) {
|
|
431
|
+
const lines: string[] = [
|
|
432
|
+
`File: ${input.filePath}\n`,
|
|
433
|
+
'=== PREVIEW ===\n',
|
|
434
|
+
result.previewText ?? '',
|
|
435
|
+
];
|
|
436
|
+
if ((result.mermaidCount ?? 0) > 0) {
|
|
437
|
+
lines.push(
|
|
438
|
+
`\n[Note: ${result.mermaidCount} mermaid diagram(s) detected — they will be rendered when published.]`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
lines.push(
|
|
442
|
+
'\n\nCall again with skip_preview: true to publish to Confluence.'
|
|
443
|
+
);
|
|
444
|
+
return { content: [{ type: 'text', text: lines.join('') }] };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
content: [
|
|
449
|
+
{
|
|
450
|
+
type: 'text',
|
|
451
|
+
text: [
|
|
452
|
+
`Successfully published "${title}" to Confluence.`,
|
|
453
|
+
`File: ${input.filePath}`,
|
|
454
|
+
`Page ID: ${result.pageId}`,
|
|
455
|
+
`Version: ${result.version}`,
|
|
456
|
+
`Mermaid diagrams rendered: ${result.mermaidCount}`,
|
|
457
|
+
`URL: ${result.url}`,
|
|
458
|
+
].join('\n'),
|
|
459
|
+
},
|
|
460
|
+
],
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
isError: true,
|
|
466
|
+
content: [{ type: 'text', text: `Error: Unknown tool "${name}"` }],
|
|
467
|
+
};
|
|
468
|
+
} catch (err: unknown) {
|
|
469
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
470
|
+
return {
|
|
471
|
+
isError: true,
|
|
472
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// ---------------------------------------------------------------------------
|
|
478
|
+
// Start
|
|
479
|
+
// ---------------------------------------------------------------------------
|
|
480
|
+
|
|
481
|
+
const transport = new StdioServerTransport();
|
|
482
|
+
await server.connect(transport);
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./src"
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|