@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 CHANGED
@@ -3,13 +3,22 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@satiyap/confluence-reader-mcp.svg)](https://www.npmjs.com/package/@satiyap/confluence-reader-mcp)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 at: https://support.atlassian.com/confluence/kb/scoped-api-tokens-in-confluence-cloud/
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="you@company.com"
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://yourteam.atlassian.net` |
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 single Confluence page by URL and returns its content as text. Also lists any direct child pages at the bottom of the response.
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 markdown document with nested headings.
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, fetchChildPages, buildAuthHeaders, buildBase } from "./confluence/client.js";
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
- }, async ({ url }) => {
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 page = await fetchPageById(cfg, pageId);
52
- const children = await fetchChildPages(cfg, pageId);
53
- const storage = page.body?.storage?.value ?? "";
54
- const markdown = storage ? storageToText(storage) : "";
55
- const childLinks = children.map(c => `- [${c.title}] (id: ${c.id})`).join("\n");
56
- const body = childLinks
57
- ? `${markdown}\n\n## Child Pages\n${childLinks}`
58
- : markdown;
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: body }]
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
- async function buildTree(id, remaining) {
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@satiyap/confluence-reader-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "MCP server for fetching and comparing Confluence documentation with local files",
5
5
  "author": "satiyap",
6
6
  "license": "MIT",