@satiyap/confluence-reader-mcp 0.1.1 → 0.1.2
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 +40 -170
- package/dist/confluence/client.js +36 -0
- package/dist/index.js +41 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,39 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@satiyap/confluence-reader-mcp)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://www.typescriptlang.org/)
|
|
6
|
-
[](https://nodejs.org/)
|
|
7
5
|
|
|
8
|
-
MCP server
|
|
6
|
+
An MCP server that lets AI assistants read Confluence pages, walk page trees, and diff Confluence content against local documentation.
|
|
9
7
|
|
|
10
|
-
##
|
|
8
|
+
## Setup
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
- **Clean text extraction**: Converts Confluence storage HTML to readable text/markdown
|
|
14
|
-
- **Git-style diffs**: Generate unified diffs comparing Confluence docs with local documentation
|
|
15
|
-
- **Flexible auth**: Supports scoped API tokens with Bearer authentication
|
|
16
|
-
- **Dual routing**: Works with cloudId routing or direct baseUrl
|
|
17
|
-
- **Zero install**: Use via `npx` for frictionless setup
|
|
10
|
+
### 1. Get a Confluence API Token
|
|
18
11
|
|
|
19
|
-
|
|
12
|
+
Create a scoped API token at: https://support.atlassian.com/confluence/kb/scoped-api-tokens-in-confluence-cloud/
|
|
20
13
|
|
|
21
|
-
###
|
|
14
|
+
### 2. Set Environment Variables
|
|
22
15
|
|
|
23
|
-
Add
|
|
16
|
+
Add to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.):
|
|
24
17
|
|
|
25
18
|
```bash
|
|
26
|
-
export CONFLUENCE_TOKEN="
|
|
27
|
-
export CONFLUENCE_EMAIL="
|
|
19
|
+
export CONFLUENCE_TOKEN="your_scoped_token"
|
|
20
|
+
export CONFLUENCE_EMAIL="you@company.com"
|
|
28
21
|
export CONFLUENCE_CLOUD_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
29
22
|
```
|
|
30
23
|
|
|
31
|
-
|
|
24
|
+
Reload your shell or open a new terminal.
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
| Variable | Required | Description |
|
|
27
|
+
|----------|----------|-------------|
|
|
28
|
+
| `CONFLUENCE_TOKEN` | Yes | Scoped API token |
|
|
29
|
+
| `CONFLUENCE_EMAIL` | Yes | Email tied to your Atlassian account |
|
|
30
|
+
| `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` |
|
|
36
32
|
|
|
37
|
-
Add to
|
|
33
|
+
### 3. Add to MCP Config
|
|
38
34
|
|
|
39
35
|
```json
|
|
40
36
|
{
|
|
@@ -47,173 +43,47 @@ Add to your MCP settings (`mcp.json`). No `env` block needed — credentials com
|
|
|
47
43
|
}
|
|
48
44
|
```
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
Restart the MCP host to pick up the new server.
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Environment Variables
|
|
55
|
-
|
|
56
|
-
| Variable | Required | Description |
|
|
57
|
-
|----------|----------|-------------|
|
|
58
|
-
| `CONFLUENCE_TOKEN` | ✅ Yes | [Scoped API token](https://support.atlassian.com/confluence/kb/scoped-api-tokens-in-confluence-cloud/) |
|
|
59
|
-
| `CONFLUENCE_EMAIL` | ✅ Yes | Email address associated with your Atlassian account |
|
|
60
|
-
| `CONFLUENCE_CLOUD_ID` | Recommended | Atlassian Cloud ID for api.atlassian.com routing |
|
|
61
|
-
| `CONFLUENCE_BASE_URL` | Optional | Fallback: `https://yourtenant.atlassian.net` |
|
|
48
|
+
## Tools
|
|
62
49
|
|
|
63
|
-
|
|
64
|
-
- Uses scoped API tokens with Basic authentication (email:token)
|
|
65
|
-
- Scoped tokens provide granular access control and better security than legacy API tokens
|
|
50
|
+
### `confluence.fetch_page`
|
|
66
51
|
|
|
67
|
-
|
|
68
|
-
- If `CONFLUENCE_CLOUD_ID` is set → Uses `https://api.atlassian.com/ex/confluence/{cloudId}`
|
|
69
|
-
- Otherwise uses `CONFLUENCE_BASE_URL`
|
|
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.
|
|
70
53
|
|
|
71
|
-
|
|
54
|
+
| Parameter | Type | Description |
|
|
55
|
+
|-----------|------|-------------|
|
|
56
|
+
| `url` | string | Confluence page URL |
|
|
72
57
|
|
|
73
|
-
### `confluence.
|
|
58
|
+
### `confluence.fetch_page_tree`
|
|
74
59
|
|
|
75
|
-
|
|
60
|
+
Fetches a page and all its descendants recursively, up to a given depth. Returns a single markdown document with nested headings.
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
- `includeStorageHtml` (boolean, optional): If true, also returns original storage HTML
|
|
62
|
+
| Parameter | Type | Default | Description |
|
|
63
|
+
|-----------|------|---------|-------------|
|
|
64
|
+
| `url` | string | — | Confluence page URL |
|
|
65
|
+
| `depth` | number | 1 | How many levels of children to fetch |
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
```json
|
|
85
|
-
{
|
|
86
|
-
"pageId": "123456789",
|
|
87
|
-
"title": "Page Title",
|
|
88
|
-
"status": "current",
|
|
89
|
-
"version": 42,
|
|
90
|
-
"webui": "/wiki/spaces/...",
|
|
91
|
-
"extractedText": "Clean text content...",
|
|
92
|
-
"storageHtml": "..." // if includeStorageHtml=true
|
|
93
|
-
}
|
|
94
|
-
```
|
|
67
|
+
### `confluence.compare`
|
|
95
68
|
|
|
96
|
-
|
|
69
|
+
Generates a git-style unified diff between a Confluence page and a local markdown string.
|
|
97
70
|
|
|
98
|
-
|
|
71
|
+
| Parameter | Type | Description |
|
|
72
|
+
|-----------|------|-------------|
|
|
73
|
+
| `url` | string | Confluence page URL |
|
|
74
|
+
| `localContent` | string | Local markdown to compare against |
|
|
99
75
|
|
|
100
|
-
|
|
101
|
-
- `confluenceText` (string, required): Text from `confluence.fetch_doc.extractedText`
|
|
102
|
-
- `prd` (string, optional): Local document text (e.g., PRD, requirements)
|
|
103
|
-
- `systemOverview` (string, optional): Local document text (e.g., architecture overview)
|
|
104
|
-
- `systemDesign` (string, optional): Local document text (e.g., technical design)
|
|
105
|
-
- `lld` (string, optional): Local document text (e.g., detailed design, implementation notes)
|
|
76
|
+
Returns a JSON object with `additions`, `deletions`, `totalChanges`, and the full `diff`.
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
**Returns:**
|
|
110
|
-
```json
|
|
111
|
-
{
|
|
112
|
-
"totalComparisons": 2,
|
|
113
|
-
"diffs": [
|
|
114
|
-
{
|
|
115
|
-
"document": "PRD",
|
|
116
|
-
"additions": 15,
|
|
117
|
-
"deletions": 8,
|
|
118
|
-
"totalChanges": 23,
|
|
119
|
-
"diff": "--- a/confluence\n+++ b/prd\n@@ -1,5 +1,5 @@\n context line\n-removed line\n+added line\n context line"
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
"document": "System Design",
|
|
123
|
-
"additions": 42,
|
|
124
|
-
"deletions": 12,
|
|
125
|
-
"totalChanges": 54,
|
|
126
|
-
"diff": "..."
|
|
127
|
-
}
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Usage Example
|
|
133
|
-
|
|
134
|
-
When a user provides a Confluence URL in their prompt:
|
|
135
|
-
|
|
136
|
-
1. AI assistant detects the URL
|
|
137
|
-
2. Calls `confluence.fetch_doc` with the URL
|
|
138
|
-
3. Calls `docs.build_comparison_bundle` with:
|
|
139
|
-
- `confluenceText` from step 2
|
|
140
|
-
- Local documentation content from filesystem
|
|
141
|
-
4. AI assistant analyzes the structured comparison and reports differences
|
|
142
|
-
|
|
143
|
-
## Supported Confluence URL Formats
|
|
78
|
+
## Supported URL Formats
|
|
144
79
|
|
|
145
80
|
- `/wiki/spaces/SPACEKEY/pages/123456789/Page+Title`
|
|
146
81
|
- `/wiki/pages/viewpage.action?pageId=123456789`
|
|
147
|
-
- Any URL containing `/pages/<numeric-id>/`
|
|
148
|
-
|
|
149
|
-
## Project Structure
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
confluence-reader-mcp/
|
|
153
|
-
├── src/
|
|
154
|
-
│ ├── index.ts # MCP server + tool registrations
|
|
155
|
-
│ ├── confluence/
|
|
156
|
-
│ │ ├── client.ts # HTTP client with scoped token auth
|
|
157
|
-
│ │ ├── url.ts # URL → pageId parser
|
|
158
|
-
│ │ ├── types.ts # API response types
|
|
159
|
-
│ │ └── transform.ts # Storage HTML → text converter
|
|
160
|
-
│ └── compare/
|
|
161
|
-
│ └── diff.ts # Git-style unified diff generator
|
|
162
|
-
├── dist/ # Compiled output
|
|
163
|
-
├── package.json # Binary: confluence-reader-mcp
|
|
164
|
-
├── tsconfig.json
|
|
165
|
-
├── .gitignore
|
|
166
|
-
└── README.md
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
## Development
|
|
170
|
-
|
|
171
|
-
```bash
|
|
172
|
-
npm run dev # Run with tsx (no build needed)
|
|
173
|
-
npm run build # Compile TypeScript
|
|
174
|
-
npm start # Run compiled server
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Security Notes
|
|
178
|
-
|
|
179
|
-
- ✅ Credentials read from OS environment variables only — never in config files
|
|
180
|
-
- ✅ Never commit tokens to git
|
|
181
|
-
- ✅ Use scoped API tokens with minimal permissions
|
|
182
|
-
|
|
183
|
-
## Publishing to npm (Optional)
|
|
184
|
-
|
|
185
|
-
Once ready for public use:
|
|
186
|
-
|
|
187
|
-
1. Update `package.json` with your repository URL and author info
|
|
188
|
-
2. Build the package:
|
|
189
|
-
```bash
|
|
190
|
-
npm run build
|
|
191
|
-
```
|
|
192
|
-
3. Publish to npm:
|
|
193
|
-
```bash
|
|
194
|
-
npm publish --access public
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Then users can use this minimal config (no env block needed):
|
|
198
|
-
```json
|
|
199
|
-
{
|
|
200
|
-
"mcpServers": {
|
|
201
|
-
"confluence-reader": {
|
|
202
|
-
"command": "npx",
|
|
203
|
-
"args": ["@satiyap/confluence-reader-mcp"]
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
**Note:** Users must set `CONFLUENCE_TOKEN`, `CONFLUENCE_EMAIL`, and `CONFLUENCE_CLOUD_ID` as OS environment variables.
|
|
210
82
|
|
|
211
|
-
##
|
|
83
|
+
## Security
|
|
212
84
|
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
- [Confluence REST API v2](https://developer.atlassian.com/cloud/confluence/rest/v2/)
|
|
216
|
-
- [Atlassian Scoped API Tokens](https://support.atlassian.com/confluence/kb/scoped-api-tokens-in-confluence-cloud/)
|
|
85
|
+
- Credentials are read from environment variables only — never passed in config files.
|
|
86
|
+
- Use scoped tokens with the minimum permissions needed.
|
|
217
87
|
|
|
218
88
|
## License
|
|
219
89
|
|
|
@@ -51,3 +51,39 @@ export async function fetchPageById(cfg, pageId) {
|
|
|
51
51
|
}
|
|
52
52
|
return (await res.json());
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Fetch direct child pages of a Confluence page using the v2 REST API.
|
|
56
|
+
* Returns all children (paginates automatically).
|
|
57
|
+
*/
|
|
58
|
+
export async function fetchChildPages(cfg, pageId) {
|
|
59
|
+
const base = buildBase(cfg);
|
|
60
|
+
const all = [];
|
|
61
|
+
let cursor;
|
|
62
|
+
while (true) {
|
|
63
|
+
const url = new URL(`${base}/wiki/api/v2/pages/${pageId}/children`);
|
|
64
|
+
url.searchParams.set("limit", "50");
|
|
65
|
+
if (cursor)
|
|
66
|
+
url.searchParams.set("cursor", cursor);
|
|
67
|
+
const res = await fetch(url.toString(), {
|
|
68
|
+
method: "GET",
|
|
69
|
+
headers: {
|
|
70
|
+
...buildAuthHeaders(cfg),
|
|
71
|
+
Accept: "application/json",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const text = await res.text().catch(() => "");
|
|
76
|
+
throw new Error(`Confluence API error ${res.status}: ${text.slice(0, 500)}`);
|
|
77
|
+
}
|
|
78
|
+
const data = (await res.json());
|
|
79
|
+
all.push(...data.results);
|
|
80
|
+
if (!data._links?.next)
|
|
81
|
+
break;
|
|
82
|
+
// The next link contains the cursor parameter
|
|
83
|
+
const nextUrl = new URL(data._links.next, base);
|
|
84
|
+
cursor = nextUrl.searchParams.get("cursor") ?? undefined;
|
|
85
|
+
if (!cursor)
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
return all;
|
|
89
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,12 @@ 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, buildAuthHeaders, buildBase } from "./confluence/client.js";
|
|
6
|
+
import { fetchPageById, fetchChildPages, 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({
|
|
10
10
|
name: "confluence-reader-mcp",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.2"
|
|
12
12
|
});
|
|
13
13
|
function getEnv(name) {
|
|
14
14
|
const v = process.env[name];
|
|
@@ -46,20 +46,55 @@ server.tool("confluence.fetch_page", "Fetch a Confluence page and return it as m
|
|
|
46
46
|
const email = getEnv("CONFLUENCE_EMAIL");
|
|
47
47
|
const cloudId = getEnv("CONFLUENCE_CLOUD_ID");
|
|
48
48
|
const baseUrl = getEnv("CONFLUENCE_BASE_URL");
|
|
49
|
+
const cfg = { token, email, cloudId, baseUrl };
|
|
49
50
|
const pageId = extractConfluencePageId(url);
|
|
50
|
-
const page = await fetchPageById(
|
|
51
|
+
const page = await fetchPageById(cfg, pageId);
|
|
52
|
+
const children = await fetchChildPages(cfg, pageId);
|
|
51
53
|
const storage = page.body?.storage?.value ?? "";
|
|
52
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;
|
|
53
59
|
return {
|
|
54
|
-
content: [{ type: "text", text:
|
|
60
|
+
content: [{ type: "text", text: body }]
|
|
55
61
|
};
|
|
56
62
|
});
|
|
57
63
|
server.tool("confluence.fetch_page_tree", "Fetch a Confluence page and all its child pages recursively up to a specified depth.", {
|
|
58
64
|
url: z.string().describe("Confluence page URL"),
|
|
59
65
|
depth: z.number().optional().default(1).describe("How many levels deep to fetch child pages (default: 1)")
|
|
60
66
|
}, async ({ url, depth }) => {
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
const token = getEnv("CONFLUENCE_TOKEN");
|
|
68
|
+
const email = getEnv("CONFLUENCE_EMAIL");
|
|
69
|
+
const cloudId = getEnv("CONFLUENCE_CLOUD_ID");
|
|
70
|
+
const baseUrl = getEnv("CONFLUENCE_BASE_URL");
|
|
71
|
+
const cfg = { token, email, cloudId, baseUrl };
|
|
72
|
+
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);
|
|
87
|
+
function renderTree(node, level) {
|
|
88
|
+
const heading = "#".repeat(Math.min(level + 1, 6));
|
|
89
|
+
const parts = [`${heading} ${node.title}`, node.content];
|
|
90
|
+
for (const child of node.children) {
|
|
91
|
+
parts.push(renderTree(child, level + 1));
|
|
92
|
+
}
|
|
93
|
+
return parts.join("\n\n");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: renderTree(tree, 0) }]
|
|
97
|
+
};
|
|
63
98
|
});
|
|
64
99
|
server.tool("confluence.compare", "Compare a local markdown file or string with a Confluence page and show the differences.", {
|
|
65
100
|
url: z.string().describe("Confluence page URL"),
|