@vinhnguyen/confluence-mcp 1.0.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 +179 -0
  2. package/index.js +954 -0
  3. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # Confluence MCP Server
2
+
3
+ A Model Context Protocol (MCP) server that provides tools for interacting with Confluence. Read, create, update, and delete pages, manage spaces, search content, and more.
4
+
5
+ ## Features
6
+
7
+ - **Page Operations**: Get, create, update, delete pages
8
+ - **Space Operations**: List spaces, get space details, browse space content
9
+ - **Search**: Full CQL support, search by text or title
10
+ - **Labels**: Get, add, remove page labels
11
+ - **Comments**: Read and add page comments
12
+ - **Attachments**: List page attachments
13
+ - **Special**: Extract DONE sections from daily reports
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ cd confluence-mcp
19
+ npm install
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ The MCP server requires three environment variables:
25
+
26
+ | Variable | Description | Example |
27
+ |----------|-------------|---------|
28
+ | `ATLASSIAN_EMAIL` | Your Atlassian account email | `user@company.com` |
29
+ | `ATLASSIAN_API_TOKEN` | API token from Atlassian | `ATATT3xFfGF0...` |
30
+ | `ATLASSIAN_DOMAIN` | Your Confluence domain | `company.atlassian.net` |
31
+
32
+ ### Getting an API Token
33
+
34
+ 1. Go to https://id.atlassian.com/manage-profile/security/api-tokens
35
+ 2. Click "Create API token"
36
+ 3. Give it a label and copy the token
37
+
38
+ ## Usage with Claude Code
39
+
40
+ Add to your Claude Code MCP settings (`~/.claude/claude_desktop_config.json` or project settings):
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "confluence": {
46
+ "command": "node",
47
+ "args": ["/path/to/confluence-mcp/index.js"],
48
+ "env": {
49
+ "ATLASSIAN_EMAIL": "your.email@company.com",
50
+ "ATLASSIAN_API_TOKEN": "your_api_token",
51
+ "ATLASSIAN_DOMAIN": "your-domain.atlassian.net"
52
+ }
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Available Tools
59
+
60
+ ### Page Operations
61
+
62
+ | Tool | Description |
63
+ |------|-------------|
64
+ | `confluence_get_page` | Get page details including content, version, metadata |
65
+ | `confluence_get_page_content` | Get page content as text or HTML |
66
+ | `confluence_get_child_pages` | Get all child pages of a parent (with pagination) |
67
+ | `confluence_create_page` | Create a new page in a space |
68
+ | `confluence_update_page` | Update an existing page |
69
+ | `confluence_delete_page` | Delete a page (moves to trash) |
70
+
71
+ ### Space Operations
72
+
73
+ | Tool | Description |
74
+ |------|-------------|
75
+ | `confluence_list_spaces` | List all accessible spaces |
76
+ | `confluence_get_space` | Get space details |
77
+ | `confluence_get_space_content` | Get pages or blogposts in a space |
78
+
79
+ ### Search Operations
80
+
81
+ | Tool | Description |
82
+ |------|-------------|
83
+ | `confluence_search` | Search using CQL query |
84
+ | `confluence_search_by_text` | Search pages by text content |
85
+ | `confluence_search_by_title` | Search pages by title |
86
+
87
+ ### Labels Operations
88
+
89
+ | Tool | Description |
90
+ |------|-------------|
91
+ | `confluence_get_page_labels` | Get labels on a page |
92
+ | `confluence_add_page_label` | Add a label to a page |
93
+ | `confluence_remove_page_label` | Remove a label from a page |
94
+
95
+ ### Comments & Attachments
96
+
97
+ | Tool | Description |
98
+ |------|-------------|
99
+ | `confluence_get_page_comments` | Get comments on a page |
100
+ | `confluence_add_page_comment` | Add a comment to a page |
101
+ | `confluence_get_page_attachments` | List attachments on a page |
102
+
103
+ ### Special Tools
104
+
105
+ | Tool | Description |
106
+ |------|-------------|
107
+ | `confluence_extract_done_sections` | Extract DONE sections from daily reports |
108
+
109
+ ## Example Usage
110
+
111
+ ### Get a page and its content
112
+
113
+ ```
114
+ Use confluence_get_page with pageId "12345678"
115
+ ```
116
+
117
+ ### Create a new page
118
+
119
+ ```
120
+ Use confluence_create_page with:
121
+ - spaceKey: "DEV"
122
+ - title: "Weekly Report - Week 5"
123
+ - content: "<h1>Weekly Summary</h1><p>This week we completed...</p>"
124
+ - parentId: "87654321" (optional)
125
+ ```
126
+
127
+ ### Search for pages
128
+
129
+ ```
130
+ Use confluence_search_by_title with title "Daily Report" and spaceKey "TEAM"
131
+ ```
132
+
133
+ ### Extract DONE sections for report generation
134
+
135
+ ```
136
+ Use confluence_extract_done_sections with pageId "12345678"
137
+ ```
138
+
139
+ ## Content Format
140
+
141
+ When creating or updating pages, use Confluence storage format (XHTML):
142
+
143
+ ```html
144
+ <h1>Heading 1</h1>
145
+ <h2>Heading 2</h2>
146
+ <p>Paragraph text</p>
147
+ <ul>
148
+ <li>List item 1</li>
149
+ <li>List item 2</li>
150
+ </ul>
151
+ <ac:structured-macro ac:name="code">
152
+ <ac:plain-text-body><![CDATA[code here]]></ac:plain-text-body>
153
+ </ac:structured-macro>
154
+ ```
155
+
156
+ ## CQL Query Examples
157
+
158
+ Confluence Query Language (CQL) is used for advanced searches:
159
+
160
+ ```
161
+ # Pages in a specific space
162
+ type=page AND space=DEV
163
+
164
+ # Pages modified recently
165
+ type=page AND lastmodified > now("-7d")
166
+
167
+ # Pages with specific label
168
+ type=page AND label=weekly-report
169
+
170
+ # Pages by creator
171
+ type=page AND creator=currentUser()
172
+
173
+ # Combined query
174
+ type=page AND space=TEAM AND text ~ "report" AND lastmodified > now("-30d")
175
+ ```
176
+
177
+ ## License
178
+
179
+ MIT
package/index.js ADDED
@@ -0,0 +1,954 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } from "@modelcontextprotocol/sdk/types.js";
9
+ import { convert } from "html-to-text";
10
+
11
+ // ============================================================================
12
+ // Configuration
13
+ // ============================================================================
14
+
15
+ const ATLASSIAN_EMAIL = process.env.ATLASSIAN_EMAIL;
16
+ const ATLASSIAN_API_TOKEN = process.env.ATLASSIAN_API_TOKEN;
17
+ const ATLASSIAN_DOMAIN = process.env.ATLASSIAN_DOMAIN;
18
+
19
+ function getAuth() {
20
+ if (!ATLASSIAN_EMAIL || !ATLASSIAN_API_TOKEN) {
21
+ throw new Error(
22
+ "Missing required environment variables: ATLASSIAN_EMAIL and ATLASSIAN_API_TOKEN"
23
+ );
24
+ }
25
+ return Buffer.from(`${ATLASSIAN_EMAIL}:${ATLASSIAN_API_TOKEN}`).toString(
26
+ "base64"
27
+ );
28
+ }
29
+
30
+ function getBaseUrl() {
31
+ if (!ATLASSIAN_DOMAIN) {
32
+ throw new Error("Missing required environment variable: ATLASSIAN_DOMAIN");
33
+ }
34
+ return `https://${ATLASSIAN_DOMAIN}`;
35
+ }
36
+
37
+ // ============================================================================
38
+ // Confluence API Client
39
+ // ============================================================================
40
+
41
+ class ConfluenceClient {
42
+ constructor() {
43
+ this.baseUrl = getBaseUrl();
44
+ this.auth = getAuth();
45
+ }
46
+
47
+ async request(endpoint, options = {}) {
48
+ const url = endpoint.startsWith("http")
49
+ ? endpoint
50
+ : `${this.baseUrl}${endpoint}`;
51
+
52
+ const response = await fetch(url, {
53
+ ...options,
54
+ headers: {
55
+ Authorization: `Basic ${this.auth}`,
56
+ Accept: "application/json",
57
+ "Content-Type": "application/json",
58
+ ...options.headers,
59
+ },
60
+ });
61
+
62
+ if (!response.ok) {
63
+ const errorText = await response.text();
64
+ throw new Error(
65
+ `Confluence API error (${response.status}): ${errorText}`
66
+ );
67
+ }
68
+
69
+ // Handle 204 No Content
70
+ if (response.status === 204) {
71
+ return null;
72
+ }
73
+
74
+ return response.json();
75
+ }
76
+
77
+ // Convert HTML to plain text
78
+ htmlToText(html) {
79
+ return convert(html, {
80
+ wordwrap: false,
81
+ preserveNewlines: true,
82
+ selectors: [
83
+ { selector: "img", format: "skip" },
84
+ { selector: "a", options: { hideLinkHrefIfSameAsText: true } },
85
+ ],
86
+ });
87
+ }
88
+
89
+ // -------------------------------------------------------------------------
90
+ // Page Operations
91
+ // -------------------------------------------------------------------------
92
+
93
+ async getPage(pageId, expand = "body.storage,version,space") {
94
+ const data = await this.request(
95
+ `/wiki/rest/api/content/${pageId}?expand=${expand}`
96
+ );
97
+ return {
98
+ id: data.id,
99
+ title: data.title,
100
+ spaceKey: data.space?.key,
101
+ version: data.version?.number,
102
+ content: data.body?.storage?.value,
103
+ contentAsText: data.body?.storage?.value
104
+ ? this.htmlToText(data.body.storage.value)
105
+ : null,
106
+ webUrl: data._links?.webui
107
+ ? `${this.baseUrl}/wiki${data._links.webui}`
108
+ : null,
109
+ };
110
+ }
111
+
112
+ async getPageContent(pageId, format = "text") {
113
+ const data = await this.request(
114
+ `/wiki/rest/api/content/${pageId}?expand=body.storage`
115
+ );
116
+ const html = data.body?.storage?.value || "";
117
+ return format === "html" ? html : this.htmlToText(html);
118
+ }
119
+
120
+ async getChildPages(parentId, limit = 250) {
121
+ const allChildren = [];
122
+ let nextUrl = `/wiki/api/v2/pages/${parentId}/children?limit=${limit}`;
123
+
124
+ while (nextUrl) {
125
+ const url = nextUrl.startsWith("http")
126
+ ? nextUrl
127
+ : `${this.baseUrl}${nextUrl}`;
128
+ const data = await this.request(url);
129
+
130
+ if (data.results) {
131
+ allChildren.push(
132
+ ...data.results.map((page) => ({
133
+ id: page.id,
134
+ title: page.title,
135
+ status: page.status,
136
+ }))
137
+ );
138
+ }
139
+
140
+ nextUrl = data._links?.next || null;
141
+ }
142
+
143
+ return allChildren;
144
+ }
145
+
146
+ async createPage(spaceKey, title, content, parentId = null) {
147
+ const body = {
148
+ type: "page",
149
+ title,
150
+ space: { key: spaceKey },
151
+ body: {
152
+ storage: {
153
+ value: content,
154
+ representation: "storage",
155
+ },
156
+ },
157
+ };
158
+
159
+ if (parentId) {
160
+ body.ancestors = [{ id: parentId }];
161
+ }
162
+
163
+ const data = await this.request("/wiki/rest/api/content", {
164
+ method: "POST",
165
+ body: JSON.stringify(body),
166
+ });
167
+
168
+ return {
169
+ id: data.id,
170
+ title: data.title,
171
+ webUrl: data._links?.webui
172
+ ? `${this.baseUrl}/wiki${data._links.webui}`
173
+ : null,
174
+ };
175
+ }
176
+
177
+ async updatePage(pageId, title, content, version) {
178
+ const body = {
179
+ type: "page",
180
+ title,
181
+ body: {
182
+ storage: {
183
+ value: content,
184
+ representation: "storage",
185
+ },
186
+ },
187
+ version: {
188
+ number: version + 1,
189
+ },
190
+ };
191
+
192
+ const data = await this.request(`/wiki/rest/api/content/${pageId}`, {
193
+ method: "PUT",
194
+ body: JSON.stringify(body),
195
+ });
196
+
197
+ return {
198
+ id: data.id,
199
+ title: data.title,
200
+ version: data.version?.number,
201
+ webUrl: data._links?.webui
202
+ ? `${this.baseUrl}/wiki${data._links.webui}`
203
+ : null,
204
+ };
205
+ }
206
+
207
+ async deletePage(pageId) {
208
+ await this.request(`/wiki/rest/api/content/${pageId}`, {
209
+ method: "DELETE",
210
+ });
211
+ return { success: true, deletedPageId: pageId };
212
+ }
213
+
214
+ // -------------------------------------------------------------------------
215
+ // Space Operations
216
+ // -------------------------------------------------------------------------
217
+
218
+ async listSpaces(limit = 25) {
219
+ const data = await this.request(`/wiki/rest/api/space?limit=${limit}`);
220
+ return data.results.map((space) => ({
221
+ key: space.key,
222
+ name: space.name,
223
+ type: space.type,
224
+ webUrl: space._links?.webui
225
+ ? `${this.baseUrl}/wiki${space._links.webui}`
226
+ : null,
227
+ }));
228
+ }
229
+
230
+ async getSpace(spaceKey) {
231
+ const data = await this.request(
232
+ `/wiki/rest/api/space/${spaceKey}?expand=description.plain,homepage`
233
+ );
234
+ return {
235
+ key: data.key,
236
+ name: data.name,
237
+ type: data.type,
238
+ description: data.description?.plain?.value,
239
+ homepageId: data.homepage?.id,
240
+ webUrl: data._links?.webui
241
+ ? `${this.baseUrl}/wiki${data._links.webui}`
242
+ : null,
243
+ };
244
+ }
245
+
246
+ async getSpaceContent(spaceKey, type = "page", limit = 25) {
247
+ const data = await this.request(
248
+ `/wiki/rest/api/space/${spaceKey}/content/${type}?limit=${limit}`
249
+ );
250
+ return data.results.map((item) => ({
251
+ id: item.id,
252
+ title: item.title,
253
+ type: item.type,
254
+ }));
255
+ }
256
+
257
+ // -------------------------------------------------------------------------
258
+ // Search Operations
259
+ // -------------------------------------------------------------------------
260
+
261
+ async search(cql, limit = 25) {
262
+ const encodedCql = encodeURIComponent(cql);
263
+ const data = await this.request(
264
+ `/wiki/rest/api/content/search?cql=${encodedCql}&limit=${limit}`
265
+ );
266
+ return data.results.map((item) => ({
267
+ id: item.id,
268
+ title: item.title,
269
+ type: item.type,
270
+ excerpt: item.excerpt,
271
+ }));
272
+ }
273
+
274
+ async searchByText(text, spaceKey = null, limit = 25) {
275
+ let cql = `text ~ "${text}"`;
276
+ if (spaceKey) {
277
+ cql += ` AND space = "${spaceKey}"`;
278
+ }
279
+ return this.search(cql, limit);
280
+ }
281
+
282
+ async searchByTitle(title, spaceKey = null, limit = 25) {
283
+ let cql = `title ~ "${title}"`;
284
+ if (spaceKey) {
285
+ cql += ` AND space = "${spaceKey}"`;
286
+ }
287
+ return this.search(cql, limit);
288
+ }
289
+
290
+ // -------------------------------------------------------------------------
291
+ // Labels Operations
292
+ // -------------------------------------------------------------------------
293
+
294
+ async getPageLabels(pageId) {
295
+ const data = await this.request(
296
+ `/wiki/rest/api/content/${pageId}/label`
297
+ );
298
+ return data.results.map((label) => ({
299
+ name: label.name,
300
+ prefix: label.prefix,
301
+ }));
302
+ }
303
+
304
+ async addPageLabel(pageId, labelName) {
305
+ const data = await this.request(
306
+ `/wiki/rest/api/content/${pageId}/label`,
307
+ {
308
+ method: "POST",
309
+ body: JSON.stringify([{ name: labelName, prefix: "global" }]),
310
+ }
311
+ );
312
+ return data.results.map((label) => ({
313
+ name: label.name,
314
+ prefix: label.prefix,
315
+ }));
316
+ }
317
+
318
+ async removePageLabel(pageId, labelName) {
319
+ await this.request(
320
+ `/wiki/rest/api/content/${pageId}/label/${labelName}`,
321
+ {
322
+ method: "DELETE",
323
+ }
324
+ );
325
+ return { success: true };
326
+ }
327
+
328
+ // -------------------------------------------------------------------------
329
+ // Comments Operations
330
+ // -------------------------------------------------------------------------
331
+
332
+ async getPageComments(pageId, limit = 25) {
333
+ const data = await this.request(
334
+ `/wiki/rest/api/content/${pageId}/child/comment?expand=body.storage&limit=${limit}`
335
+ );
336
+ return data.results.map((comment) => ({
337
+ id: comment.id,
338
+ content: comment.body?.storage?.value
339
+ ? this.htmlToText(comment.body.storage.value)
340
+ : null,
341
+ }));
342
+ }
343
+
344
+ async addPageComment(pageId, content) {
345
+ const body = {
346
+ type: "comment",
347
+ container: { id: pageId, type: "page" },
348
+ body: {
349
+ storage: {
350
+ value: content,
351
+ representation: "storage",
352
+ },
353
+ },
354
+ };
355
+
356
+ const data = await this.request("/wiki/rest/api/content", {
357
+ method: "POST",
358
+ body: JSON.stringify(body),
359
+ });
360
+
361
+ return {
362
+ id: data.id,
363
+ };
364
+ }
365
+
366
+ // -------------------------------------------------------------------------
367
+ // Attachments Operations
368
+ // -------------------------------------------------------------------------
369
+
370
+ async getPageAttachments(pageId, limit = 25) {
371
+ const data = await this.request(
372
+ `/wiki/rest/api/content/${pageId}/child/attachment?limit=${limit}`
373
+ );
374
+ return data.results.map((attachment) => ({
375
+ id: attachment.id,
376
+ title: attachment.title,
377
+ mediaType: attachment.metadata?.mediaType,
378
+ fileSize: attachment.extensions?.fileSize,
379
+ downloadUrl: attachment._links?.download
380
+ ? `${this.baseUrl}/wiki${attachment._links.download}`
381
+ : null,
382
+ }));
383
+ }
384
+
385
+ // -------------------------------------------------------------------------
386
+ // Extract DONE sections (from existing app)
387
+ // -------------------------------------------------------------------------
388
+
389
+ async extractDoneSections(pageId) {
390
+ const content = await this.getPageContent(pageId, "text");
391
+ const doneRegex = /DONE([\s\S]*?)(?=TODO|$)/g;
392
+ const matches = [];
393
+ let match;
394
+
395
+ while ((match = doneRegex.exec(content)) !== null) {
396
+ matches.push(match[1].trim());
397
+ }
398
+
399
+ return {
400
+ pageId,
401
+ doneSections: matches,
402
+ fullContent: content,
403
+ };
404
+ }
405
+ }
406
+
407
+ // ============================================================================
408
+ // MCP Server Setup
409
+ // ============================================================================
410
+
411
+ const server = new Server(
412
+ {
413
+ name: "confluence-mcp",
414
+ version: "1.0.0",
415
+ },
416
+ {
417
+ capabilities: {
418
+ tools: {},
419
+ },
420
+ }
421
+ );
422
+
423
+ const client = new ConfluenceClient();
424
+
425
+ // ============================================================================
426
+ // Tool Definitions
427
+ // ============================================================================
428
+
429
+ const TOOLS = [
430
+ // Page Operations
431
+ {
432
+ name: "confluence_get_page",
433
+ description:
434
+ "Get a Confluence page by ID, including its content, version, and metadata",
435
+ inputSchema: {
436
+ type: "object",
437
+ properties: {
438
+ pageId: {
439
+ type: "string",
440
+ description: "The ID of the page to retrieve",
441
+ },
442
+ },
443
+ required: ["pageId"],
444
+ },
445
+ },
446
+ {
447
+ name: "confluence_get_page_content",
448
+ description:
449
+ "Get the content of a Confluence page as plain text or HTML",
450
+ inputSchema: {
451
+ type: "object",
452
+ properties: {
453
+ pageId: {
454
+ type: "string",
455
+ description: "The ID of the page",
456
+ },
457
+ format: {
458
+ type: "string",
459
+ enum: ["text", "html"],
460
+ description: "Output format: 'text' (default) or 'html'",
461
+ },
462
+ },
463
+ required: ["pageId"],
464
+ },
465
+ },
466
+ {
467
+ name: "confluence_get_child_pages",
468
+ description:
469
+ "Get all child pages of a parent page (handles pagination automatically)",
470
+ inputSchema: {
471
+ type: "object",
472
+ properties: {
473
+ parentId: {
474
+ type: "string",
475
+ description: "The ID of the parent page",
476
+ },
477
+ },
478
+ required: ["parentId"],
479
+ },
480
+ },
481
+ {
482
+ name: "confluence_create_page",
483
+ description:
484
+ "Create a new Confluence page in a space, optionally as a child of another page",
485
+ inputSchema: {
486
+ type: "object",
487
+ properties: {
488
+ spaceKey: {
489
+ type: "string",
490
+ description: "The key of the space (e.g., 'DEV', 'TEAM')",
491
+ },
492
+ title: {
493
+ type: "string",
494
+ description: "The title of the new page",
495
+ },
496
+ content: {
497
+ type: "string",
498
+ description:
499
+ "The content in Confluence storage format (XHTML). Use <p> tags for paragraphs, <h1>-<h6> for headings, etc.",
500
+ },
501
+ parentId: {
502
+ type: "string",
503
+ description:
504
+ "Optional: ID of the parent page if creating a child page",
505
+ },
506
+ },
507
+ required: ["spaceKey", "title", "content"],
508
+ },
509
+ },
510
+ {
511
+ name: "confluence_update_page",
512
+ description:
513
+ "Update an existing Confluence page content and/or title",
514
+ inputSchema: {
515
+ type: "object",
516
+ properties: {
517
+ pageId: {
518
+ type: "string",
519
+ description: "The ID of the page to update",
520
+ },
521
+ title: {
522
+ type: "string",
523
+ description: "The new title of the page",
524
+ },
525
+ content: {
526
+ type: "string",
527
+ description: "The new content in Confluence storage format (XHTML)",
528
+ },
529
+ version: {
530
+ type: "number",
531
+ description:
532
+ "The current version number of the page (required for update)",
533
+ },
534
+ },
535
+ required: ["pageId", "title", "content", "version"],
536
+ },
537
+ },
538
+ {
539
+ name: "confluence_delete_page",
540
+ description:
541
+ "Delete a Confluence page by ID (moves to trash)",
542
+ inputSchema: {
543
+ type: "object",
544
+ properties: {
545
+ pageId: {
546
+ type: "string",
547
+ description: "The ID of the page to delete",
548
+ },
549
+ },
550
+ required: ["pageId"],
551
+ },
552
+ },
553
+
554
+ // Space Operations
555
+ {
556
+ name: "confluence_list_spaces",
557
+ description: "List all accessible Confluence spaces",
558
+ inputSchema: {
559
+ type: "object",
560
+ properties: {
561
+ limit: {
562
+ type: "number",
563
+ description: "Maximum number of spaces to return (default: 25)",
564
+ },
565
+ },
566
+ },
567
+ },
568
+ {
569
+ name: "confluence_get_space",
570
+ description: "Get details about a specific Confluence space",
571
+ inputSchema: {
572
+ type: "object",
573
+ properties: {
574
+ spaceKey: {
575
+ type: "string",
576
+ description: "The key of the space (e.g., 'DEV', 'TEAM')",
577
+ },
578
+ },
579
+ required: ["spaceKey"],
580
+ },
581
+ },
582
+ {
583
+ name: "confluence_get_space_content",
584
+ description: "Get pages or blogposts in a specific space",
585
+ inputSchema: {
586
+ type: "object",
587
+ properties: {
588
+ spaceKey: {
589
+ type: "string",
590
+ description: "The key of the space",
591
+ },
592
+ type: {
593
+ type: "string",
594
+ enum: ["page", "blogpost"],
595
+ description: "Content type: 'page' (default) or 'blogpost'",
596
+ },
597
+ limit: {
598
+ type: "number",
599
+ description: "Maximum number of items to return (default: 25)",
600
+ },
601
+ },
602
+ required: ["spaceKey"],
603
+ },
604
+ },
605
+
606
+ // Search Operations
607
+ {
608
+ name: "confluence_search",
609
+ description:
610
+ "Search Confluence using CQL (Confluence Query Language)",
611
+ inputSchema: {
612
+ type: "object",
613
+ properties: {
614
+ cql: {
615
+ type: "string",
616
+ description:
617
+ 'CQL query string (e.g., \'type=page AND space=DEV AND text ~ "report"\')',
618
+ },
619
+ limit: {
620
+ type: "number",
621
+ description: "Maximum number of results (default: 25)",
622
+ },
623
+ },
624
+ required: ["cql"],
625
+ },
626
+ },
627
+ {
628
+ name: "confluence_search_by_text",
629
+ description: "Search Confluence pages by text content",
630
+ inputSchema: {
631
+ type: "object",
632
+ properties: {
633
+ text: {
634
+ type: "string",
635
+ description: "Text to search for",
636
+ },
637
+ spaceKey: {
638
+ type: "string",
639
+ description: "Optional: Limit search to a specific space",
640
+ },
641
+ limit: {
642
+ type: "number",
643
+ description: "Maximum number of results (default: 25)",
644
+ },
645
+ },
646
+ required: ["text"],
647
+ },
648
+ },
649
+ {
650
+ name: "confluence_search_by_title",
651
+ description: "Search Confluence pages by title",
652
+ inputSchema: {
653
+ type: "object",
654
+ properties: {
655
+ title: {
656
+ type: "string",
657
+ description: "Title text to search for",
658
+ },
659
+ spaceKey: {
660
+ type: "string",
661
+ description: "Optional: Limit search to a specific space",
662
+ },
663
+ limit: {
664
+ type: "number",
665
+ description: "Maximum number of results (default: 25)",
666
+ },
667
+ },
668
+ required: ["title"],
669
+ },
670
+ },
671
+
672
+ // Labels Operations
673
+ {
674
+ name: "confluence_get_page_labels",
675
+ description: "Get all labels attached to a page",
676
+ inputSchema: {
677
+ type: "object",
678
+ properties: {
679
+ pageId: {
680
+ type: "string",
681
+ description: "The ID of the page",
682
+ },
683
+ },
684
+ required: ["pageId"],
685
+ },
686
+ },
687
+ {
688
+ name: "confluence_add_page_label",
689
+ description: "Add a label to a page",
690
+ inputSchema: {
691
+ type: "object",
692
+ properties: {
693
+ pageId: {
694
+ type: "string",
695
+ description: "The ID of the page",
696
+ },
697
+ labelName: {
698
+ type: "string",
699
+ description: "The label name to add",
700
+ },
701
+ },
702
+ required: ["pageId", "labelName"],
703
+ },
704
+ },
705
+ {
706
+ name: "confluence_remove_page_label",
707
+ description: "Remove a label from a page",
708
+ inputSchema: {
709
+ type: "object",
710
+ properties: {
711
+ pageId: {
712
+ type: "string",
713
+ description: "The ID of the page",
714
+ },
715
+ labelName: {
716
+ type: "string",
717
+ description: "The label name to remove",
718
+ },
719
+ },
720
+ required: ["pageId", "labelName"],
721
+ },
722
+ },
723
+
724
+ // Comments Operations
725
+ {
726
+ name: "confluence_get_page_comments",
727
+ description: "Get comments on a page",
728
+ inputSchema: {
729
+ type: "object",
730
+ properties: {
731
+ pageId: {
732
+ type: "string",
733
+ description: "The ID of the page",
734
+ },
735
+ limit: {
736
+ type: "number",
737
+ description: "Maximum number of comments to return (default: 25)",
738
+ },
739
+ },
740
+ required: ["pageId"],
741
+ },
742
+ },
743
+ {
744
+ name: "confluence_add_page_comment",
745
+ description: "Add a comment to a page",
746
+ inputSchema: {
747
+ type: "object",
748
+ properties: {
749
+ pageId: {
750
+ type: "string",
751
+ description: "The ID of the page",
752
+ },
753
+ content: {
754
+ type: "string",
755
+ description: "The comment content in HTML format",
756
+ },
757
+ },
758
+ required: ["pageId", "content"],
759
+ },
760
+ },
761
+
762
+ // Attachments Operations
763
+ {
764
+ name: "confluence_get_page_attachments",
765
+ description: "Get attachments on a page",
766
+ inputSchema: {
767
+ type: "object",
768
+ properties: {
769
+ pageId: {
770
+ type: "string",
771
+ description: "The ID of the page",
772
+ },
773
+ limit: {
774
+ type: "number",
775
+ description: "Maximum number of attachments to return (default: 25)",
776
+ },
777
+ },
778
+ required: ["pageId"],
779
+ },
780
+ },
781
+
782
+ // Special Operations
783
+ {
784
+ name: "confluence_extract_done_sections",
785
+ description:
786
+ "Extract DONE sections from a page (useful for daily reports). Returns content between 'DONE' and 'TODO' markers.",
787
+ inputSchema: {
788
+ type: "object",
789
+ properties: {
790
+ pageId: {
791
+ type: "string",
792
+ description: "The ID of the page to extract DONE sections from",
793
+ },
794
+ },
795
+ required: ["pageId"],
796
+ },
797
+ },
798
+ ];
799
+
800
+ // ============================================================================
801
+ // Tool Handlers
802
+ // ============================================================================
803
+
804
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
805
+ return { tools: TOOLS };
806
+ });
807
+
808
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
809
+ const { name, arguments: args } = request.params;
810
+
811
+ try {
812
+ let result;
813
+
814
+ switch (name) {
815
+ // Page Operations
816
+ case "confluence_get_page":
817
+ result = await client.getPage(args.pageId);
818
+ break;
819
+
820
+ case "confluence_get_page_content":
821
+ result = await client.getPageContent(args.pageId, args.format || "text");
822
+ break;
823
+
824
+ case "confluence_get_child_pages":
825
+ result = await client.getChildPages(args.parentId);
826
+ break;
827
+
828
+ case "confluence_create_page":
829
+ result = await client.createPage(
830
+ args.spaceKey,
831
+ args.title,
832
+ args.content,
833
+ args.parentId
834
+ );
835
+ break;
836
+
837
+ case "confluence_update_page":
838
+ result = await client.updatePage(
839
+ args.pageId,
840
+ args.title,
841
+ args.content,
842
+ args.version
843
+ );
844
+ break;
845
+
846
+ case "confluence_delete_page":
847
+ result = await client.deletePage(args.pageId);
848
+ break;
849
+
850
+ // Space Operations
851
+ case "confluence_list_spaces":
852
+ result = await client.listSpaces(args.limit);
853
+ break;
854
+
855
+ case "confluence_get_space":
856
+ result = await client.getSpace(args.spaceKey);
857
+ break;
858
+
859
+ case "confluence_get_space_content":
860
+ result = await client.getSpaceContent(
861
+ args.spaceKey,
862
+ args.type || "page",
863
+ args.limit
864
+ );
865
+ break;
866
+
867
+ // Search Operations
868
+ case "confluence_search":
869
+ result = await client.search(args.cql, args.limit);
870
+ break;
871
+
872
+ case "confluence_search_by_text":
873
+ result = await client.searchByText(args.text, args.spaceKey, args.limit);
874
+ break;
875
+
876
+ case "confluence_search_by_title":
877
+ result = await client.searchByTitle(
878
+ args.title,
879
+ args.spaceKey,
880
+ args.limit
881
+ );
882
+ break;
883
+
884
+ // Labels Operations
885
+ case "confluence_get_page_labels":
886
+ result = await client.getPageLabels(args.pageId);
887
+ break;
888
+
889
+ case "confluence_add_page_label":
890
+ result = await client.addPageLabel(args.pageId, args.labelName);
891
+ break;
892
+
893
+ case "confluence_remove_page_label":
894
+ result = await client.removePageLabel(args.pageId, args.labelName);
895
+ break;
896
+
897
+ // Comments Operations
898
+ case "confluence_get_page_comments":
899
+ result = await client.getPageComments(args.pageId, args.limit);
900
+ break;
901
+
902
+ case "confluence_add_page_comment":
903
+ result = await client.addPageComment(args.pageId, args.content);
904
+ break;
905
+
906
+ // Attachments Operations
907
+ case "confluence_get_page_attachments":
908
+ result = await client.getPageAttachments(args.pageId, args.limit);
909
+ break;
910
+
911
+ // Special Operations
912
+ case "confluence_extract_done_sections":
913
+ result = await client.extractDoneSections(args.pageId);
914
+ break;
915
+
916
+ default:
917
+ throw new Error(`Unknown tool: ${name}`);
918
+ }
919
+
920
+ return {
921
+ content: [
922
+ {
923
+ type: "text",
924
+ text: JSON.stringify(result, null, 2),
925
+ },
926
+ ],
927
+ };
928
+ } catch (error) {
929
+ return {
930
+ content: [
931
+ {
932
+ type: "text",
933
+ text: `Error: ${error.message}`,
934
+ },
935
+ ],
936
+ isError: true,
937
+ };
938
+ }
939
+ });
940
+
941
+ // ============================================================================
942
+ // Start Server
943
+ // ============================================================================
944
+
945
+ async function main() {
946
+ const transport = new StdioServerTransport();
947
+ await server.connect(transport);
948
+ console.error("Confluence MCP server running on stdio");
949
+ }
950
+
951
+ main().catch((error) => {
952
+ console.error("Fatal error:", error);
953
+ process.exit(1);
954
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@vinhnguyen/confluence-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Confluence - read, create, update, delete pages and manage spaces",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "confluence-mcp": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "confluence",
16
+ "atlassian",
17
+ "model-context-protocol",
18
+ "claude",
19
+ "ai",
20
+ "llm"
21
+ ],
22
+ "author": "vinhnguyen",
23
+ "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
+ "bugs": {
30
+ "url": "https://github.com/vinhnguyen/confluence-mcp/issues"
31
+ },
32
+ "files": [
33
+ "index.js",
34
+ "README.md"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ },
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.0.0",
41
+ "html-to-text": "^9.0.5"
42
+ }
43
+ }