@vinhnguyen/confluence-mcp 1.0.0 → 1.1.0

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.
Files changed (3) hide show
  1. package/README.md +114 -5
  2. package/index.js +342 -0
  3. package/package.json +45 -15
package/README.md CHANGED
@@ -5,23 +5,39 @@ A Model Context Protocol (MCP) server that provides tools for interacting with C
5
5
  ## Features
6
6
 
7
7
  - **Page Operations**: Get, create, update, delete pages
8
+ - **Page Hierarchy**: Move pages, list child pages, get page with children
8
9
  - **Space Operations**: List spaces, get space details, browse space content
9
10
  - **Search**: Full CQL support, search by text or title
10
- - **Labels**: Get, add, remove page labels
11
+ - **Labels**: Get, add, remove, and batch manage page labels
11
12
  - **Comments**: Read and add page comments
12
13
  - **Attachments**: List page attachments
13
14
  - **Special**: Extract DONE sections from daily reports
14
15
 
15
16
  ## Installation
16
17
 
18
+ ### Using npx (recommended, no install required)
19
+
20
+ No installation needed! Just configure Claude Desktop or Claude Code as shown below.
21
+
22
+ ### Global Installation
23
+
24
+ ```bash
25
+ npm install -g @vinhnguyen/confluence-mcp
26
+ ```
27
+
28
+ ### From Source
29
+
17
30
  ```bash
31
+ git clone https://github.com/vinhnguyen/confluence-mcp.git
18
32
  cd confluence-mcp
19
33
  npm install
20
34
  ```
21
35
 
22
36
  ## Configuration
23
37
 
24
- The MCP server requires three environment variables:
38
+ ### Prerequisites
39
+
40
+ The MCP server requires Atlassian credentials:
25
41
 
26
42
  | Variable | Description | Example |
27
43
  |----------|-------------|---------|
@@ -35,9 +51,49 @@ The MCP server requires three environment variables:
35
51
  2. Click "Create API token"
36
52
  3. Give it a label and copy the token
37
53
 
38
- ## Usage with Claude Code
54
+ ### Claude Desktop
55
+
56
+ Add the server to your Claude Desktop configuration file:
57
+
58
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
59
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
60
+
61
+ Using npx (recommended, no install required):
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "confluence": {
67
+ "command": "npx",
68
+ "args": ["-y", "@vinhnguyen/confluence-mcp"],
69
+ "env": {
70
+ "ATLASSIAN_EMAIL": "your.email@company.com",
71
+ "ATLASSIAN_API_TOKEN": "your_api_token",
72
+ "ATLASSIAN_DOMAIN": "your-domain.atlassian.net"
73
+ }
74
+ }
75
+ }
76
+ }
77
+ ```
39
78
 
40
- Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json` or project settings):
79
+ Or if installed globally via npm:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "confluence": {
85
+ "command": "confluence-mcp",
86
+ "env": {
87
+ "ATLASSIAN_EMAIL": "your.email@company.com",
88
+ "ATLASSIAN_API_TOKEN": "your_api_token",
89
+ "ATLASSIAN_DOMAIN": "your-domain.atlassian.net"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ Or from source:
41
97
 
42
98
  ```json
43
99
  {
@@ -55,6 +111,55 @@ Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json` or
55
111
  }
56
112
  ```
57
113
 
114
+ ### Claude Code
115
+
116
+ Add to your `~/.claude.json` (project) or `~/.claude/settings.json` (global):
117
+
118
+ Using npx (recommended):
119
+
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "confluence": {
124
+ "command": "npx",
125
+ "args": ["-y", "@vinhnguyen/confluence-mcp"],
126
+ "env": {
127
+ "ATLASSIAN_EMAIL": "your.email@company.com",
128
+ "ATLASSIAN_API_TOKEN": "your_api_token",
129
+ "ATLASSIAN_DOMAIN": "your-domain.atlassian.net"
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ Or if installed globally:
137
+
138
+ ```json
139
+ {
140
+ "mcpServers": {
141
+ "confluence": {
142
+ "command": "confluence-mcp",
143
+ "env": {
144
+ "ATLASSIAN_EMAIL": "your.email@company.com",
145
+ "ATLASSIAN_API_TOKEN": "your_api_token",
146
+ "ATLASSIAN_DOMAIN": "your-domain.atlassian.net"
147
+ }
148
+ }
149
+ }
150
+ }
151
+ ```
152
+
153
+ **Restart Claude Desktop or Claude Code after updating the config.**
154
+
155
+ ## Environment Variables
156
+
157
+ | Variable | Description | Required |
158
+ |----------|-------------|----------|
159
+ | `ATLASSIAN_EMAIL` | Your Atlassian account email | Yes |
160
+ | `ATLASSIAN_API_TOKEN` | API token from Atlassian | Yes |
161
+ | `ATLASSIAN_DOMAIN` | Your Confluence domain (e.g., `company.atlassian.net`) | Yes |
162
+
58
163
  ## Available Tools
59
164
 
60
165
  ### Page Operations
@@ -64,8 +169,12 @@ Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json` or
64
169
  | `confluence_get_page` | Get page details including content, version, metadata |
65
170
  | `confluence_get_page_content` | Get page content as text or HTML |
66
171
  | `confluence_get_child_pages` | Get all child pages of a parent (with pagination) |
172
+ | `confluence_get_page_with_children` | Get a page along with all its immediate child pages |
173
+ | `confluence_list_child_pages` | List immediate child pages (folder contents view) |
67
174
  | `confluence_create_page` | Create a new page in a space |
68
- | `confluence_update_page` | Update an existing page |
175
+ | `confluence_update_page` | Update an existing page (requires version number) |
176
+ | `confluence_update_page_auto` | Update a page with automatic version handling |
177
+ | `confluence_move_page` | Move a page to a new parent (restructure hierarchy) |
69
178
  | `confluence_delete_page` | Delete a page (moves to trash) |
70
179
 
71
180
  ### Space Operations
package/index.js CHANGED
@@ -204,6 +204,22 @@ class ConfluenceClient {
204
204
  };
205
205
  }
206
206
 
207
+ // Auto-versioning update: fetches current version automatically
208
+ async updatePageAuto(pageId, newTitle = null, newBody = null) {
209
+ // Fetch current page to get version and existing data
210
+ const currentPage = await this.getPage(pageId);
211
+
212
+ if (!currentPage) {
213
+ throw new Error(`Page with ID ${pageId} not found`);
214
+ }
215
+
216
+ const title = newTitle || currentPage.title;
217
+ const content = newBody || currentPage.content;
218
+ const currentVersion = currentPage.version;
219
+
220
+ return this.updatePage(pageId, title, content, currentVersion);
221
+ }
222
+
207
223
  async deletePage(pageId) {
208
224
  await this.request(`/wiki/rest/api/content/${pageId}`, {
209
225
  method: "DELETE",
@@ -211,6 +227,126 @@ class ConfluenceClient {
211
227
  return { success: true, deletedPageId: pageId };
212
228
  }
213
229
 
230
+ // List immediate child pages (using API v2 for better pagination)
231
+ async listChildPages(pageId, limit = 50) {
232
+ const allChildren = [];
233
+ let nextUrl = `/wiki/api/v2/pages/${pageId}/children?limit=${limit}`;
234
+
235
+ while (nextUrl) {
236
+ const url = nextUrl.startsWith("http")
237
+ ? nextUrl
238
+ : `${this.baseUrl}${nextUrl}`;
239
+ const data = await this.request(url);
240
+
241
+ if (data.results) {
242
+ allChildren.push(
243
+ ...data.results.map((page) => ({
244
+ id: page.id,
245
+ title: page.title,
246
+ status: page.status,
247
+ parentId: page.parentId,
248
+ spaceId: page.spaceId,
249
+ }))
250
+ );
251
+ }
252
+
253
+ nextUrl = data._links?.next || null;
254
+ }
255
+
256
+ return {
257
+ parentPageId: pageId,
258
+ childCount: allChildren.length,
259
+ children: allChildren,
260
+ };
261
+ }
262
+
263
+ // Move a page to a new parent (restructure hierarchy)
264
+ async movePage(pageId, newParentId) {
265
+ // First, get current page details
266
+ const currentPage = await this.request(
267
+ `/wiki/rest/api/content/${pageId}?expand=version,space,ancestors`
268
+ );
269
+
270
+ if (!currentPage) {
271
+ throw new Error(`Page with ID ${pageId} not found`);
272
+ }
273
+
274
+ // Update page with new parent (ancestors)
275
+ const body = {
276
+ type: "page",
277
+ title: currentPage.title,
278
+ ancestors: [{ id: newParentId }],
279
+ version: {
280
+ number: currentPage.version.number + 1,
281
+ },
282
+ };
283
+
284
+ const data = await this.request(`/wiki/rest/api/content/${pageId}`, {
285
+ method: "PUT",
286
+ body: JSON.stringify(body),
287
+ });
288
+
289
+ return {
290
+ id: data.id,
291
+ title: data.title,
292
+ newParentId: newParentId,
293
+ version: data.version?.number,
294
+ webUrl: data._links?.webui
295
+ ? `${this.baseUrl}/wiki${data._links.webui}`
296
+ : null,
297
+ };
298
+ }
299
+
300
+ // Manage labels: add or remove multiple labels at once
301
+ async manageLabels(pageId, action, labels) {
302
+ if (!Array.isArray(labels) || labels.length === 0) {
303
+ throw new Error("Labels must be a non-empty array");
304
+ }
305
+
306
+ const results = {
307
+ pageId,
308
+ action,
309
+ processed: [],
310
+ errors: [],
311
+ };
312
+
313
+ if (action === "add") {
314
+ // Add all labels at once
315
+ const labelData = labels.map((name) => ({ name, prefix: "global" }));
316
+ try {
317
+ const data = await this.request(
318
+ `/wiki/rest/api/content/${pageId}/label`,
319
+ {
320
+ method: "POST",
321
+ body: JSON.stringify(labelData),
322
+ }
323
+ );
324
+ results.processed = data.results.map((label) => label.name);
325
+ } catch (error) {
326
+ results.errors.push({ labels, error: error.message });
327
+ }
328
+ } else if (action === "remove") {
329
+ // Remove labels one by one (API limitation)
330
+ for (const labelName of labels) {
331
+ try {
332
+ await this.request(
333
+ `/wiki/rest/api/content/${pageId}/label/${labelName}`,
334
+ {
335
+ method: "DELETE",
336
+ }
337
+ );
338
+ results.processed.push(labelName);
339
+ } catch (error) {
340
+ results.errors.push({ label: labelName, error: error.message });
341
+ }
342
+ }
343
+ } else {
344
+ throw new Error(`Invalid action: ${action}. Use 'add' or 'remove'.`);
345
+ }
346
+
347
+ return results;
348
+ }
349
+
214
350
  // -------------------------------------------------------------------------
215
351
  // Space Operations
216
352
  // -------------------------------------------------------------------------
@@ -402,6 +538,77 @@ class ConfluenceClient {
402
538
  fullContent: content,
403
539
  };
404
540
  }
541
+
542
+ // -------------------------------------------------------------------------
543
+ // Get page with children
544
+ // -------------------------------------------------------------------------
545
+
546
+ async getPageWithChildren(pageId, includeFullContent = false) {
547
+ // Fetch the parent page
548
+ const parentPage = await this.getPage(pageId);
549
+
550
+ // Fetch child pages (using existing getChildPages method which handles pagination)
551
+ let childPages = [];
552
+ try {
553
+ childPages = await this.getChildPages(pageId);
554
+ } catch (error) {
555
+ // If fetching children fails (e.g., no children or permission issues), continue with empty array
556
+ childPages = [];
557
+ }
558
+
559
+ // If includeFullContent is true, fetch full content for each child page
560
+ let childPagesWithContent = [];
561
+ if (includeFullContent && childPages.length > 0) {
562
+ childPagesWithContent = await Promise.all(
563
+ childPages.map(async (child) => {
564
+ try {
565
+ const fullChild = await this.getPage(child.id);
566
+ return {
567
+ id: fullChild.id,
568
+ title: fullChild.title,
569
+ spaceKey: fullChild.spaceKey,
570
+ version: fullChild.version,
571
+ content: fullChild.content,
572
+ contentAsText: fullChild.contentAsText,
573
+ webUrl: fullChild.webUrl,
574
+ };
575
+ } catch (error) {
576
+ // If fetching a child fails, return basic info with error
577
+ return {
578
+ id: child.id,
579
+ title: child.title,
580
+ status: child.status,
581
+ error: `Failed to fetch content: ${error.message}`,
582
+ };
583
+ }
584
+ })
585
+ );
586
+ } else {
587
+ // Just return basic info (id, title, status)
588
+ childPagesWithContent = childPages.map((child) => ({
589
+ id: child.id,
590
+ title: child.title,
591
+ status: child.status,
592
+ }));
593
+ }
594
+
595
+ return {
596
+ parent: {
597
+ id: parentPage.id,
598
+ title: parentPage.title,
599
+ spaceKey: parentPage.spaceKey,
600
+ version: parentPage.version,
601
+ content: parentPage.content,
602
+ contentAsText: parentPage.contentAsText,
603
+ webUrl: parentPage.webUrl,
604
+ },
605
+ childPages: {
606
+ count: childPagesWithContent.length,
607
+ includeFullContent,
608
+ pages: childPagesWithContent,
609
+ },
610
+ };
611
+ }
405
612
  }
406
613
 
407
614
  // ============================================================================
@@ -550,6 +757,94 @@ const TOOLS = [
550
757
  required: ["pageId"],
551
758
  },
552
759
  },
760
+ {
761
+ name: "confluence_update_page_auto",
762
+ description:
763
+ "Update an existing Confluence page with automatic version handling. Fetches the current version automatically so you don't need to provide it. You can update title, body, or both.",
764
+ inputSchema: {
765
+ type: "object",
766
+ properties: {
767
+ pageId: {
768
+ type: "string",
769
+ description: "The ID of the page to update",
770
+ },
771
+ newTitle: {
772
+ type: "string",
773
+ description: "The new title for the page (optional - keeps existing title if not provided)",
774
+ },
775
+ newBody: {
776
+ type: "string",
777
+ description: "The new content in Confluence storage format (XHTML). Optional - keeps existing content if not provided.",
778
+ },
779
+ },
780
+ required: ["pageId"],
781
+ },
782
+ },
783
+ {
784
+ name: "confluence_list_child_pages",
785
+ description:
786
+ "List all immediate child pages of a parent page (folder contents view). Returns page IDs, titles, and status for each child.",
787
+ inputSchema: {
788
+ type: "object",
789
+ properties: {
790
+ pageId: {
791
+ type: "string",
792
+ description: "The ID of the parent page to list children from",
793
+ },
794
+ limit: {
795
+ type: "number",
796
+ description: "Maximum number of children per API request (default: 50). Pagination is handled automatically.",
797
+ },
798
+ },
799
+ required: ["pageId"],
800
+ },
801
+ },
802
+ {
803
+ name: "confluence_move_page",
804
+ description:
805
+ "Move a page to a new parent, restructuring the page hierarchy. The page will become a child of the specified new parent page.",
806
+ inputSchema: {
807
+ type: "object",
808
+ properties: {
809
+ pageId: {
810
+ type: "string",
811
+ description: "The ID of the page to move",
812
+ },
813
+ newParentId: {
814
+ type: "string",
815
+ description: "The ID of the new parent page",
816
+ },
817
+ },
818
+ required: ["pageId", "newParentId"],
819
+ },
820
+ },
821
+ {
822
+ name: "confluence_manage_labels",
823
+ description:
824
+ "Add or remove multiple labels from a page in a single operation. Use action 'add' to add labels or 'remove' to remove them.",
825
+ inputSchema: {
826
+ type: "object",
827
+ properties: {
828
+ pageId: {
829
+ type: "string",
830
+ description: "The ID of the page to manage labels on",
831
+ },
832
+ action: {
833
+ type: "string",
834
+ enum: ["add", "remove"],
835
+ description: "The action to perform: 'add' to add labels, 'remove' to remove labels",
836
+ },
837
+ labels: {
838
+ type: "array",
839
+ items: {
840
+ type: "string",
841
+ },
842
+ description: "Array of label names to add or remove",
843
+ },
844
+ },
845
+ required: ["pageId", "action", "labels"],
846
+ },
847
+ },
553
848
 
554
849
  // Space Operations
555
850
  {
@@ -795,6 +1090,26 @@ const TOOLS = [
795
1090
  required: ["pageId"],
796
1091
  },
797
1092
  },
1093
+ {
1094
+ name: "confluence_get_page_with_children",
1095
+ description:
1096
+ "Get a Confluence page along with all its immediate child pages in a single request. Returns the parent page content followed by a list of child pages. Optionally fetches full content for each child page.",
1097
+ inputSchema: {
1098
+ type: "object",
1099
+ properties: {
1100
+ pageId: {
1101
+ type: "string",
1102
+ description: "The ID of the parent page to retrieve",
1103
+ },
1104
+ includeFullContent: {
1105
+ type: "boolean",
1106
+ description:
1107
+ "If true, fetch full content for each child page. If false (default), only return child page IDs and titles.",
1108
+ },
1109
+ },
1110
+ required: ["pageId"],
1111
+ },
1112
+ },
798
1113
  ];
799
1114
 
800
1115
  // ============================================================================
@@ -847,6 +1162,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
847
1162
  result = await client.deletePage(args.pageId);
848
1163
  break;
849
1164
 
1165
+ case "confluence_update_page_auto":
1166
+ result = await client.updatePageAuto(
1167
+ args.pageId,
1168
+ args.newTitle,
1169
+ args.newBody
1170
+ );
1171
+ break;
1172
+
1173
+ case "confluence_list_child_pages":
1174
+ result = await client.listChildPages(args.pageId, args.limit);
1175
+ break;
1176
+
1177
+ case "confluence_move_page":
1178
+ result = await client.movePage(args.pageId, args.newParentId);
1179
+ break;
1180
+
1181
+ case "confluence_manage_labels":
1182
+ result = await client.manageLabels(args.pageId, args.action, args.labels);
1183
+ break;
1184
+
850
1185
  // Space Operations
851
1186
  case "confluence_list_spaces":
852
1187
  result = await client.listSpaces(args.limit);
@@ -913,6 +1248,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
913
1248
  result = await client.extractDoneSections(args.pageId);
914
1249
  break;
915
1250
 
1251
+ case "confluence_get_page_with_children":
1252
+ result = await client.getPageWithChildren(
1253
+ args.pageId,
1254
+ args.includeFullContent || false
1255
+ );
1256
+ break;
1257
+
916
1258
  default:
917
1259
  throw new Error(`Unknown tool: ${name}`);
918
1260
  }
package/package.json CHANGED
@@ -1,14 +1,19 @@
1
1
  {
2
2
  "name": "@vinhnguyen/confluence-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for Confluence - read, create, update, delete pages and manage spaces",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "confluence-mcp": "./index.js"
9
9
  },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/glorynguyen/confluence-mcp.git"
13
+ },
10
14
  "scripts": {
11
- "start": "node index.js"
15
+ "start": "node index.js",
16
+ "semantic-release": "semantic-release"
12
17
  },
13
18
  "keywords": [
14
19
  "mcp",
@@ -19,25 +24,50 @@
19
24
  "ai",
20
25
  "llm"
21
26
  ],
22
- "author": "vinhnguyen",
27
+ "author": "Vinh Nguyen",
23
28
  "license": "MIT",
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/vinhnguyen/confluence-mcp"
27
- },
28
- "homepage": "https://github.com/vinhnguyen/confluence-mcp#readme",
29
+ "homepage": "https://github.com/glorynguyen/confluence-mcp#readme",
29
30
  "bugs": {
30
- "url": "https://github.com/vinhnguyen/confluence-mcp/issues"
31
+ "url": "https://github.com/glorynguyen/confluence-mcp/issues"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0",
35
+ "html-to-text": "^9.0.5"
36
+ },
37
+ "devDependencies": {
38
+ "@semantic-release/changelog": "^6.0.3",
39
+ "@semantic-release/git": "^10.0.1",
40
+ "@semantic-release/github": "^12.0.3",
41
+ "semantic-release": "^25.0.3"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
31
45
  },
32
46
  "files": [
33
47
  "index.js",
34
48
  "README.md"
35
49
  ],
36
- "engines": {
37
- "node": ">=18.0.0"
38
- },
39
- "dependencies": {
40
- "@modelcontextprotocol/sdk": "^1.0.0",
41
- "html-to-text": "^9.0.5"
50
+ "release": {
51
+ "branches": [
52
+ "main"
53
+ ],
54
+ "plugins": [
55
+ "@semantic-release/commit-analyzer",
56
+ "@semantic-release/release-notes-generator",
57
+ "@semantic-release/changelog",
58
+ "@semantic-release/npm",
59
+ [
60
+ "@semantic-release/git",
61
+ {
62
+ "assets": [
63
+ "package.json",
64
+ "package-lock.json",
65
+ "CHANGELOG.md"
66
+ ],
67
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
68
+ }
69
+ ],
70
+ "@semantic-release/github"
71
+ ]
42
72
  }
43
73
  }