@satiyap/confluence-reader-mcp 0.1.2 → 0.1.3
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/README.md +23 -9
- package/dist/confluence/client.js +54 -0
- package/dist/index.js +16 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,13 +3,22 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@satiyap/confluence-reader-mcp)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
|
|
6
|
-
An MCP server that lets AI assistants read Confluence pages, walk page trees, and diff Confluence content against local documentation.
|
|
6
|
+
An MCP server that lets AI assistants read Confluence Cloud pages, walk page trees recursively, and diff Confluence content against local documentation.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- Fetch a single Confluence page as plain text
|
|
11
|
+
- Recursively fetch entire page trees up to a configurable depth
|
|
12
|
+
- Compare local content against a Confluence page with a unified diff
|
|
13
|
+
- Parallel child-page fetching with error resilience
|
|
14
|
+
- Supports scoped API tokens with Basic Auth
|
|
7
15
|
|
|
8
16
|
## Setup
|
|
9
17
|
|
|
10
18
|
### 1. Get a Confluence API Token
|
|
11
19
|
|
|
12
|
-
Create a scoped API token
|
|
20
|
+
Create a scoped API token from your Atlassian account:
|
|
21
|
+
https://support.atlassian.com/confluence/kb/scoped-api-tokens-in-confluence-cloud/
|
|
13
22
|
|
|
14
23
|
### 2. Set Environment Variables
|
|
15
24
|
|
|
@@ -17,7 +26,7 @@ Add to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.):
|
|
|
17
26
|
|
|
18
27
|
```bash
|
|
19
28
|
export CONFLUENCE_TOKEN="your_scoped_token"
|
|
20
|
-
export CONFLUENCE_EMAIL="
|
|
29
|
+
export CONFLUENCE_EMAIL="your_email@example.com"
|
|
21
30
|
export CONFLUENCE_CLOUD_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
22
31
|
```
|
|
23
32
|
|
|
@@ -28,7 +37,7 @@ Reload your shell or open a new terminal.
|
|
|
28
37
|
| `CONFLUENCE_TOKEN` | Yes | Scoped API token |
|
|
29
38
|
| `CONFLUENCE_EMAIL` | Yes | Email tied to your Atlassian account |
|
|
30
39
|
| `CONFLUENCE_CLOUD_ID` | One of these | Cloud ID — routes via `api.atlassian.com` |
|
|
31
|
-
| `CONFLUENCE_BASE_URL` | is required | Direct tenant URL, e.g. `https://
|
|
40
|
+
| `CONFLUENCE_BASE_URL` | is required | Direct tenant URL, e.g. `https://your-org.atlassian.net` |
|
|
32
41
|
|
|
33
42
|
### 3. Add to MCP Config
|
|
34
43
|
|
|
@@ -49,15 +58,16 @@ Restart the MCP host to pick up the new server.
|
|
|
49
58
|
|
|
50
59
|
### `confluence.fetch_page`
|
|
51
60
|
|
|
52
|
-
Fetches a
|
|
61
|
+
Fetches a Confluence page by URL and returns its content as text. Optionally recurses into child pages.
|
|
53
62
|
|
|
54
|
-
| Parameter | Type | Description |
|
|
55
|
-
|
|
56
|
-
| `url` | string | Confluence page URL |
|
|
63
|
+
| Parameter | Type | Default | Description |
|
|
64
|
+
|-----------|------|---------|-------------|
|
|
65
|
+
| `url` | string | — | Confluence page URL |
|
|
66
|
+
| `depth` | number | 0 | Levels of child pages to fetch recursively |
|
|
57
67
|
|
|
58
68
|
### `confluence.fetch_page_tree`
|
|
59
69
|
|
|
60
|
-
Fetches a page and all its descendants recursively, up to a given depth. Returns a single
|
|
70
|
+
Fetches a page and all its descendants recursively, up to a given depth. Returns a single document with nested headings.
|
|
61
71
|
|
|
62
72
|
| Parameter | Type | Default | Description |
|
|
63
73
|
|-----------|------|---------|-------------|
|
|
@@ -85,6 +95,10 @@ Returns a JSON object with `additions`, `deletions`, `totalChanges`, and the ful
|
|
|
85
95
|
- Credentials are read from environment variables only — never passed in config files.
|
|
86
96
|
- Use scoped tokens with the minimum permissions needed.
|
|
87
97
|
|
|
98
|
+
## Contributing
|
|
99
|
+
|
|
100
|
+
Contributions are welcome. Please open an issue or submit a pull request.
|
|
101
|
+
|
|
88
102
|
## License
|
|
89
103
|
|
|
90
104
|
MIT
|
|
@@ -87,3 +87,57 @@ export async function fetchChildPages(cfg, pageId) {
|
|
|
87
87
|
}
|
|
88
88
|
return all;
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Recursively fetch a page and its descendants up to the given depth.
|
|
92
|
+
*
|
|
93
|
+
* For each page it:
|
|
94
|
+
* 1. Fetches the full page content via fetchPageById
|
|
95
|
+
* 2. Discovers child page IDs via fetchChildPages
|
|
96
|
+
* 3. Recurses into each child (in parallel) until depth is exhausted
|
|
97
|
+
*
|
|
98
|
+
* Children at each level are fetched concurrently with a concurrency
|
|
99
|
+
* limit to avoid hammering the API. Pages that fail to load are
|
|
100
|
+
* included as stubs with an error message instead of aborting the
|
|
101
|
+
* entire tree.
|
|
102
|
+
*
|
|
103
|
+
* @param cfg - Client configuration
|
|
104
|
+
* @param pageId - Root page ID to start from
|
|
105
|
+
* @param depth - How many levels of children to fetch (0 = root only)
|
|
106
|
+
* @param concurrency - Max parallel requests per level (default 5)
|
|
107
|
+
* @returns A tree of PageNode objects
|
|
108
|
+
*/
|
|
109
|
+
export async function fetchPageTree(cfg, pageId, depth, concurrency = 5) {
|
|
110
|
+
const { storageToText } = await import("./transform.js");
|
|
111
|
+
const page = await fetchPageById(cfg, pageId);
|
|
112
|
+
const storage = page.body?.storage?.value ?? "";
|
|
113
|
+
const content = storage ? storageToText(storage) : "";
|
|
114
|
+
const children = [];
|
|
115
|
+
if (depth > 0) {
|
|
116
|
+
const childPages = await fetchChildPages(cfg, pageId);
|
|
117
|
+
// Fetch children in parallel, bounded by concurrency limit
|
|
118
|
+
const results = await parallelMap(childPages, (child) => fetchPageTree(cfg, child.id, depth - 1, concurrency).catch((err) => ({
|
|
119
|
+
id: child.id,
|
|
120
|
+
title: child.title ?? `Page ${child.id}`,
|
|
121
|
+
content: `[Error fetching page: ${err.message}]`,
|
|
122
|
+
children: [],
|
|
123
|
+
})), concurrency);
|
|
124
|
+
children.push(...results);
|
|
125
|
+
}
|
|
126
|
+
return { id: page.id, title: page.title, content, children };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Run an async mapper over items with a concurrency limit.
|
|
130
|
+
*/
|
|
131
|
+
async function parallelMap(items, fn, limit) {
|
|
132
|
+
const results = new Array(items.length);
|
|
133
|
+
let idx = 0;
|
|
134
|
+
async function worker() {
|
|
135
|
+
while (idx < items.length) {
|
|
136
|
+
const i = idx++;
|
|
137
|
+
results[i] = await fn(items[i]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
141
|
+
await Promise.all(workers);
|
|
142
|
+
return results;
|
|
143
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { extractConfluencePageId } from "./confluence/url.js";
|
|
6
|
-
import { fetchPageById,
|
|
6
|
+
import { fetchPageById, fetchPageTree, buildAuthHeaders, buildBase } from "./confluence/client.js";
|
|
7
7
|
import { storageToText } from "./confluence/transform.js";
|
|
8
8
|
import { generateUnifiedDiff, generateDiffStats } from "./compare/diff.js";
|
|
9
9
|
const server = new McpServer({
|
|
@@ -39,25 +39,27 @@ function validateEnvironment() {
|
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
-
server.tool("confluence.fetch_page", "Fetch a Confluence page and return it as markdown.", {
|
|
43
|
-
url: z.string().describe("Confluence page URL")
|
|
44
|
-
|
|
42
|
+
server.tool("confluence.fetch_page", "Fetch a Confluence page and return it as markdown. Optionally recurse into child pages.", {
|
|
43
|
+
url: z.string().describe("Confluence page URL"),
|
|
44
|
+
depth: z.number().optional().default(0).describe("Levels of child pages to fetch recursively (default: 0, root page only)")
|
|
45
|
+
}, async ({ url, depth }) => {
|
|
45
46
|
const token = getEnv("CONFLUENCE_TOKEN");
|
|
46
47
|
const email = getEnv("CONFLUENCE_EMAIL");
|
|
47
48
|
const cloudId = getEnv("CONFLUENCE_CLOUD_ID");
|
|
48
49
|
const baseUrl = getEnv("CONFLUENCE_BASE_URL");
|
|
49
50
|
const cfg = { token, email, cloudId, baseUrl };
|
|
50
51
|
const pageId = extractConfluencePageId(url);
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
const tree = await fetchPageTree(cfg, pageId, depth);
|
|
53
|
+
function renderNode(node, level) {
|
|
54
|
+
const heading = "#".repeat(Math.min(level + 1, 6));
|
|
55
|
+
const parts = [`${heading} ${node.title}`, node.content];
|
|
56
|
+
for (const child of node.children) {
|
|
57
|
+
parts.push(renderNode(child, level + 1));
|
|
58
|
+
}
|
|
59
|
+
return parts.join("\n\n");
|
|
60
|
+
}
|
|
59
61
|
return {
|
|
60
|
-
content: [{ type: "text", text:
|
|
62
|
+
content: [{ type: "text", text: renderNode(tree, 0) }]
|
|
61
63
|
};
|
|
62
64
|
});
|
|
63
65
|
server.tool("confluence.fetch_page_tree", "Fetch a Confluence page and all its child pages recursively up to a specified depth.", {
|
|
@@ -70,20 +72,7 @@ server.tool("confluence.fetch_page_tree", "Fetch a Confluence page and all its c
|
|
|
70
72
|
const baseUrl = getEnv("CONFLUENCE_BASE_URL");
|
|
71
73
|
const cfg = { token, email, cloudId, baseUrl };
|
|
72
74
|
const pageId = extractConfluencePageId(url);
|
|
73
|
-
|
|
74
|
-
const page = await fetchPageById(cfg, id);
|
|
75
|
-
const storage = page.body?.storage?.value ?? "";
|
|
76
|
-
const content = storage ? storageToText(storage) : "";
|
|
77
|
-
const children = [];
|
|
78
|
-
if (remaining > 0) {
|
|
79
|
-
const childPages = await fetchChildPages(cfg, id);
|
|
80
|
-
for (const child of childPages) {
|
|
81
|
-
children.push(await buildTree(child.id, remaining - 1));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return { id: page.id, title: page.title, content, children };
|
|
85
|
-
}
|
|
86
|
-
const tree = await buildTree(pageId, depth);
|
|
75
|
+
const tree = await fetchPageTree(cfg, pageId, depth);
|
|
87
76
|
function renderTree(node, level) {
|
|
88
77
|
const heading = "#".repeat(Math.min(level + 1, 6));
|
|
89
78
|
const parts = [`${heading} ${node.title}`, node.content];
|