@atlassian-dc-mcp/confluence 0.28.0 → 0.30.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACjJ,OAAO,EAAE,iBAAiB,EAAqB,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACtG,OAAO,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjG,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAE/C,uBAAuB,EAAE,CAAC;AAE1B,0CAA0C;AAC1C,MAAM,cAAc,GAAG,iBAAiB,CAAC,cAAc,EAAE,CAAC;AAC1D,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IAC9B,MAAM,IAAI,KAAK,CAAC,2CAA2C,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC1F,CAAC;AAED,gCAAgC;AAChC,MAAM,gBAAgB,GAAG,0BAA0B,EAAE,CAAC;AACtD,MAAM,iBAAiB,GAAG,IAAI,iBAAiB,CAC7C,gBAAgB,CAAC,IAAI,EACrB,GAAG,EAAE,CAAC,0BAA0B,EAAE,CAAC,KAAK,EACxC,gBAAgB,CAAC,WAAW,EAC5B,kBAAkB,CACnB,CAAC;AAEF,kCAAkC;AAClC,MAAM,sBAAsB,GAAG,yCAAyC,CAAC;AAEzE,wBAAwB;AACxB,MAAM,MAAM,GAAG,eAAe,CAAC;IAC7B,IAAI,EAAE,0BAA0B;IAChC,OAAO;CACR,CAAC,CAAC;AAEH,+BAA+B;AAC/B,MAAM,CAAC,IAAI,CACT,uBAAuB,EACvB,yCAAyC,sBAAsB,EAAE,EACjE,qBAAqB,CAAC,UAAU,EAChC,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,EAAE,EAAE,EAAE;IACjE,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,CAAC;IACxG,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,IAAI,CACT,0BAA0B,EAC1B,yBAAyB,sBAAsB,YAAY,EAC3D,qBAAqB,CAAC,aAAa,EACnC,KAAK,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE;IAC/C,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IACzF,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,IAAI,CACT,0BAA0B,EAC1B,yBAAyB,sBAAsB,EAAE,EACjD,qBAAqB,CAAC,aAAa,EACnC,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE;IAC7D,MAAM,UAAU,GAAsB;QACpC,IAAI,EAAE,IAAI,IAAI,MAAM;QACpB,KAAK;QACL,KAAK,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE;QACxB,IAAI,EAAE;YACJ,OAAO,EAAE;gBACP,KAAK,EAAE,OAAO;gBACd,cAAc,EAAE,SAAS;aAC1B;SACF;KACF,CAAC;IAEF,2CAA2C;IAC3C,IAAI,QAAQ,EAAE,CAAC;QACb,UAAU,CAAC,SAAS,GAAG,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACjE,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACvD,OAAO,kBAAkB,CAAC;YACxB,GAAG,MAAM;YACT,IAAI,EAAE,0BAA0B,CAAC,MAAM,CAAC,IAAI,CAAC;SAC9C,CAAC,CAAC;IACL,CAAC;IACD,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,IAAI,CACT,0BAA0B,EAC1B,8BAA8B,sBAAsB,EAAE,EACtD,qBAAqB,CAAC,aAAa,EACnC,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE;IACvE,8CAA8C;IAC9C,MAAM,cAAc,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;IAExE,IAAI,CAAC,cAAc,CAAC,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QACpD,OAAO,kBAAkB,CAAC;YACxB,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,sCAAsC,SAAS,KAAK,cAAc,CAAC,KAAK,IAAI,eAAe,EAAE;SACrG,CAAC,CAAC;IACL,CAAC;IAED,6DAA6D;IAC7D,MAAM,WAAW,GAAG,cAAc,CAAC,IAIlC,CAAC;IAEF,MAAM,SAAS,GAAsB;QACnC,EAAE,EAAE,SAAS;QACb,IAAI,EAAE,WAAW,CAAC,IAAI;QACtB,KAAK,EAAE,KAAK,IAAI,WAAW,CAAC,KAAK;QACjC,KAAK,EAAE,WAAW,CAAC,KAAK;QACxB,OAAO,EAAE;YACP,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,cAAc;SACxB;KACF,CAAC;IAEF,0CAA0C;IAC1C,IAAI,OAAO,EAAE,CAAC;QACZ,SAAS,CAAC,IAAI,GAAG;YACf,OAAO,EAAE;gBACP,KAAK,EAAE,OAAO;gBACd,cAAc,EAAE,SAAS;aAC1B;SACF,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,aAAa,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC3E,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACvD,OAAO,kBAAkB,CAAC;YACxB,GAAG,MAAM;YACT,IAAI,EAAE,0BAA0B,CAAC,MAAM,CAAC,IAAI,CAAC;SAC9C,CAAC,CAAC;IACL,CAAC;IACD,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC,CACF,CAAC;AAEF,MAAM,iBAAiB,GAAG,wBAAwB,CAAC,kBAAkB,CAAC,CAAC;AAEvE,wFAAwF;AACxF,IAAI,iBAAiB,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACrC,MAAM,CAAC,IAAI,CACT,6BAA6B,EAC7B,8EAA8E,sBAAsB,kHAAkH,EACtN,qBAAqB,CAAC,gBAAgB,EACtC,KAAK,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,EAAE,EAAE;QAC1G,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,gBAAgB,CACrD,SAAS,EACT,UAAU,EACV,iBAAiB,CAAC,MAAM,EACxB,QAAQ,EACR,OAAO,EACP,SAAS,EACT,MAAM,EACN,eAAe,EACf,eAAe,CAChB,CAAC;QACF,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,0FAA0F;AAC1F,MAAM,mBAAmB,GAAG,iBAAiB,CAAC,QAAQ,CAAC,OAAO,CAAC;AAC/D,MAAM,CAAC,IAAI,CACT,+BAA+B,EAC/B,4EAA4E,sBAAsB,+IAA+I,mBAAmB,CAAC,CAAC,CAAC,qGAAqG,CAAC,CAAC,CAAC,mDAAmD,EAAE,EACpa,EAAE,GAAG,qBAAqB,CAAC,kBAAkB,EAAE,GAAG,CAAC,mBAAmB,CAAC,CAAC,CAAC,qBAAqB,CAAC,4BAA4B,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EACnI,KAAK,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,aAAa,EAAE,cAAc,EAAE,IAAI,EAAE,QAAQ,EAO1E,EAAE,EAAE;IACH,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,6BAA6B,CAAC;QACnE,SAAS;QACT,QAAQ;QACR,IAAI;QACJ,QAAQ;QACR,aAAa;QACb,cAAc;QACd,YAAY,EAAE,iBAAiB,CAAC,QAAQ;KACzC,CAAC,CAAC;IACH,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC,CACF,CAAC;AAEF,MAAM,CAAC,IAAI,CAAC,wBAAwB,EAClC,wBAAwB,sBAAsB,EAAE,EAChD,qBAAqB,CAAC,YAAY,EAClC,KAAK,EAAE,EACE,UAAU,EACV,KAAK,EACL,KAAK,EACL,MAAM,EACN,OAAO,EACR,EAAE,EAAE;IACV,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,YAAY,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/F,OAAO,kBAAkB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC,CAAC,CAAC;AAEL,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlassian-dc-mcp/confluence",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "bin": "./bin/run.js",
@@ -31,7 +31,7 @@
31
31
  "test": "jest --passWithNoTests"
32
32
  },
33
33
  "dependencies": {
34
- "@atlassian-dc-mcp/common": "^0.28.0",
34
+ "@atlassian-dc-mcp/common": "^0.30.0",
35
35
  "@modelcontextprotocol/sdk": "^1.27.1",
36
36
  "dotenv": "^16.4.7",
37
37
  "zod": "^3.24.2"
@@ -45,5 +45,5 @@
45
45
  "publishConfig": {
46
46
  "access": "public"
47
47
  },
48
- "gitHead": "7f51cd1b0328ecc90785211511415e9609d09a25"
48
+ "gitHead": "92a4fc60548de5ee374489deef4d09271536014f"
49
49
  }
@@ -0,0 +1,89 @@
1
+ import type { AttachmentGatewaySide } from '@atlassian-dc-mcp/common';
2
+ import { ConfluenceService } from '../confluence-service.js';
3
+ import { AttachmentsService } from '../confluence-client/index.js';
4
+
5
+ const DISABLED_DOWNLOAD: AttachmentGatewaySide = { enabled: false, roots: [], maxBytes: 25 * 1024 * 1024 };
6
+
7
+ jest.mock('../confluence-client/index.js', () => ({
8
+ AttachmentsService: {
9
+ getAttachments: jest.fn(),
10
+ },
11
+ ContentResourceService: {},
12
+ SearchService: {},
13
+ UserService: {},
14
+ OpenAPI: { BASE: '', TOKEN: '', VERSION: '' },
15
+ }));
16
+
17
+ function mockFetchText(text: string) {
18
+ global.fetch = jest.fn().mockResolvedValue({
19
+ ok: true,
20
+ status: 200,
21
+ statusText: 'OK',
22
+ headers: { get: () => 'text/plain' },
23
+ arrayBuffer: async () => Buffer.from(text),
24
+ }) as unknown as typeof fetch;
25
+ }
26
+
27
+ describe('ConfluenceService.downloadAttachmentFromContent', () => {
28
+ let service: ConfluenceService;
29
+
30
+ beforeEach(() => {
31
+ service = new ConfluenceService('test-host', 'test-token');
32
+ jest.clearAllMocks();
33
+ });
34
+
35
+ afterEach(() => jest.restoreAllMocks());
36
+
37
+ it('builds an absolute download URL from the relative _links.download and returns content', async () => {
38
+ (AttachmentsService.getAttachments as jest.Mock).mockResolvedValue({
39
+ results: [
40
+ { title: 'doc.txt', metadata: { mediaType: 'text/plain' }, _links: { download: '/download/attachments/123/doc.txt?version=1' } },
41
+ ],
42
+ });
43
+ mockFetchText('file body');
44
+
45
+ const result = await service.downloadAttachmentFromContent({ contentId: '123', filename: 'doc.txt', returnContent: 'text', downloadSide: DISABLED_DOWNLOAD });
46
+
47
+ expect(AttachmentsService.getAttachments).toHaveBeenCalledWith('123', undefined, 'doc.txt');
48
+ expect(global.fetch).toHaveBeenCalledWith(
49
+ 'https://test-host/download/attachments/123/doc.txt?version=1',
50
+ expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer test-token' }) }),
51
+ );
52
+ expect(result.success).toBe(true);
53
+ expect(result.data).toMatchObject({ contentId: '123', count: 1 });
54
+ expect(result.data!.attachments[0]).toMatchObject({ filename: 'doc.txt', content: 'file body', mediaType: 'text/plain' });
55
+ });
56
+
57
+ it('downloads all attachments when no filename is given', async () => {
58
+ (AttachmentsService.getAttachments as jest.Mock).mockResolvedValue({
59
+ results: [
60
+ { title: 'a.txt', _links: { download: '/download/attachments/123/a.txt' } },
61
+ { title: 'b.txt', _links: { download: '/download/attachments/123/b.txt' } },
62
+ ],
63
+ });
64
+ mockFetchText('x');
65
+
66
+ const result = await service.downloadAttachmentFromContent({ contentId: '123', downloadSide: DISABLED_DOWNLOAD });
67
+
68
+ expect(result.success).toBe(true);
69
+ expect(result.data!.count).toBe(2);
70
+ expect(global.fetch).toHaveBeenCalledTimes(2);
71
+ });
72
+
73
+ it('returns a failure when the content has no matching attachment', async () => {
74
+ (AttachmentsService.getAttachments as jest.Mock).mockResolvedValue({ results: [] });
75
+
76
+ const result = await service.downloadAttachmentFromContent({ contentId: '123', filename: 'missing.txt', downloadSide: DISABLED_DOWNLOAD });
77
+
78
+ expect(result.success).toBe(false);
79
+ expect(result.error).toContain('No attachment named "missing.txt"');
80
+ });
81
+
82
+ it('refuses to save to disk when downloads are disabled', async () => {
83
+ const result = await service.downloadAttachmentFromContent({ contentId: '123', filename: 'doc.txt', save: true, downloadSide: DISABLED_DOWNLOAD });
84
+
85
+ expect(result.success).toBe(false);
86
+ expect(result.error).toContain('disabled');
87
+ expect(global.fetch).not.toHaveBeenCalled();
88
+ });
89
+ });
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import type { AttachmentGatewaySide } from '@atlassian-dc-mcp/common';
5
+ import { ConfluenceService } from '../confluence-service.js';
6
+ import { AttachmentsService } from '../confluence-client/index.js';
7
+
8
+ jest.mock('../confluence-client/index.js', () => ({
9
+ AttachmentsService: {
10
+ getAttachments: jest.fn(),
11
+ createAttachments: jest.fn(),
12
+ updateData: jest.fn(),
13
+ },
14
+ ContentResourceService: {},
15
+ SearchService: {},
16
+ UserService: {},
17
+ OpenAPI: { BASE: '', TOKEN: '', VERSION: '', HEADERS: undefined },
18
+ }));
19
+
20
+ describe('ConfluenceService.uploadAttachment', () => {
21
+ let service: ConfluenceService;
22
+ let root: string;
23
+ const uploadSide = (): AttachmentGatewaySide => ({ enabled: true, roots: [fs.realpathSync(root)], maxBytes: 1024 });
24
+
25
+ beforeEach(() => {
26
+ service = new ConfluenceService('test-host', 'test-token');
27
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-mcp-cup-'));
28
+ jest.clearAllMocks();
29
+ (AttachmentsService.createAttachments as jest.Mock).mockResolvedValue({ results: [{ id: '1', title: 'a.txt' }] });
30
+ });
31
+
32
+ afterEach(() => {
33
+ fs.rmSync(root, { recursive: true, force: true });
34
+ jest.restoreAllMocks();
35
+ });
36
+
37
+ it('uploads a file resolved within the upload root', async () => {
38
+ fs.writeFileSync(path.join(root, 'a.txt'), 'hello');
39
+
40
+ const result = await service.uploadAttachment('42', 'a.txt', uploadSide());
41
+
42
+ expect(result.success).toBe(true);
43
+ expect(AttachmentsService.createAttachments).toHaveBeenCalledTimes(1);
44
+ const formData = (AttachmentsService.createAttachments as jest.Mock).mock.calls[0][4];
45
+ expect(formData.file.name).toBe('a.txt');
46
+ });
47
+
48
+ it('rejects an absolute path outside the root', async () => {
49
+ const result = await service.uploadAttachment('42', '/etc/passwd', uploadSide());
50
+ expect(result.success).toBe(false);
51
+ expect(AttachmentsService.createAttachments).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it('rejects a symlink inside the root', async () => {
55
+ const target = path.join(root, 'real.txt');
56
+ fs.writeFileSync(target, 'secret');
57
+ fs.symlinkSync(target, path.join(root, 'link.txt'));
58
+
59
+ const result = await service.uploadAttachment('42', 'link.txt', uploadSide());
60
+ expect(result.success).toBe(false);
61
+ expect(AttachmentsService.createAttachments).not.toHaveBeenCalled();
62
+ });
63
+
64
+ it('rejects a file over the size limit', async () => {
65
+ fs.writeFileSync(path.join(root, 'big.txt'), 'x'.repeat(2048));
66
+
67
+ const result = await service.uploadAttachment('42', 'big.txt', uploadSide());
68
+ expect(result.success).toBe(false);
69
+ expect(AttachmentsService.createAttachments).not.toHaveBeenCalled();
70
+ });
71
+ });
@@ -1,6 +1,17 @@
1
+ import { File } from 'node:buffer';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { basename } from 'node:path';
1
4
  import { z } from 'zod';
2
- import { ContentResourceService, OpenAPI, SearchService, UserService } from './confluence-client/index.js';
3
- import { handleApiOperation, resolveOpenApiBase } from '@atlassian-dc-mcp/common';
5
+ import { AttachmentsService, ContentResourceService, OpenAPI, SearchService, UserService } from './confluence-client/index.js';
6
+ import {
7
+ downloadAttachment,
8
+ handleApiOperation,
9
+ resolveDownloadDestination,
10
+ resolveOpenApiBase,
11
+ resolveUploadSource,
12
+ type AttachmentContentEncoding,
13
+ type AttachmentGatewaySide,
14
+ } from '@atlassian-dc-mcp/common';
4
15
  import { CONFLUENCE_PRODUCT, getDefaultPageSize, getMissingConfig } from './config.js';
5
16
  import { ConfluenceBodyMode, shapeConfluenceContent } from './confluence-response-mapper.js';
6
17
 
@@ -45,6 +56,8 @@ function resolveToken(token: string | (() => string | undefined), missingTokenMe
45
56
 
46
57
  export class ConfluenceService {
47
58
  private readonly getPageSize: () => number;
59
+ private readonly baseUrl: string;
60
+ private readonly tokenProvider: string | (() => string | undefined);
48
61
 
49
62
  constructor(
50
63
  host: string | undefined,
@@ -52,16 +65,31 @@ export class ConfluenceService {
52
65
  apiBasePath?: string,
53
66
  getPageSize: () => number = getDefaultPageSize,
54
67
  ) {
55
- OpenAPI.BASE = resolveOpenApiBase({
68
+ const base = resolveOpenApiBase({
56
69
  host,
57
70
  apiBasePath,
58
71
  defaultBasePath: CONFLUENCE_PRODUCT.defaultApiBasePath ?? '',
59
72
  strippableSuffixes: CONFLUENCE_PRODUCT.apiBasePathStrippableSuffixes,
60
73
  });
74
+ OpenAPI.BASE = base;
61
75
  OpenAPI.TOKEN = resolveToken(token, 'Missing required environment variable: CONFLUENCE_API_TOKEN');
62
76
  OpenAPI.VERSION = '1.0';
77
+ this.baseUrl = base;
78
+ this.tokenProvider = token;
63
79
  this.getPageSize = getPageSize;
64
80
  }
81
+
82
+ /**
83
+ * Builds an absolute download URL from a Confluence attachment `_links.download`
84
+ * value, which is a path relative to the site base (context path included).
85
+ */
86
+ private buildDownloadUrl(downloadLink: string): string {
87
+ if (/^https?:\/\//i.test(downloadLink)) {
88
+ return downloadLink;
89
+ }
90
+ const base = this.baseUrl.replace(/\/$/, '');
91
+ return `${base}${downloadLink.startsWith('/') ? '' : '/'}${downloadLink}`;
92
+ }
65
93
  /**
66
94
  * Get a Confluence page by ID
67
95
  * @param contentId The ID of the page to retrieve
@@ -132,6 +160,132 @@ export class ConfluenceService {
132
160
  return handleApiOperation(() => ContentResourceService.update2(contentId, content), 'Error updating content');
133
161
  }
134
162
 
163
+ /**
164
+ * Upload a file as an attachment to a Confluence content entity
165
+ * @param contentId The ID of the content the attachment will be attached to
166
+ * @param sourcePath Path to the file to upload, relative to a server-configured upload directory
167
+ * @param uploadSide Resolved upload gateway (roots, size limit, enabled flag)
168
+ * @param filename Optional override for the attachment filename (defaults to basename of sourcePath)
169
+ * @param comment Optional comment describing the attachment
170
+ * @param minorEdit If true, no notification email will be generated
171
+ * @param hidden If true, no notification email or activity stream entry will be generated
172
+ * @param allowDuplicated Allow upload even if an attachment with the same filename exists
173
+ */
174
+ async uploadAttachment(
175
+ contentId: string,
176
+ sourcePath: string,
177
+ uploadSide: AttachmentGatewaySide,
178
+ filename?: string,
179
+ comment?: string,
180
+ minorEdit?: boolean,
181
+ hidden?: boolean,
182
+ allowDuplicated?: boolean,
183
+ versionIfExists?: boolean,
184
+ ) {
185
+ return handleApiOperation(async () => {
186
+ const { absolutePath } = await resolveUploadSource({ requestedPath: sourcePath, side: uploadSide });
187
+ const buffer = await readFile(absolutePath);
188
+ const name = filename || basename(absolutePath);
189
+ const file = new File([buffer], name);
190
+ // X-Atlassian-Token: nocheck is required for multipart attachment POSTs (XSRF bypass).
191
+ // Set it only for the duration of this call; restore afterwards.
192
+ const prevHeaders = OpenAPI.HEADERS;
193
+ OpenAPI.HEADERS = { 'X-Atlassian-Token': 'nocheck' };
194
+ try {
195
+ if (versionIfExists) {
196
+ const existing = await AttachmentsService.getAttachments(contentId, undefined, name);
197
+ const existingId = (existing as any)?.results?.[0]?.id;
198
+ if (existingId) {
199
+ // MockAttachmentRequest types file as string, but getFormData handles Blob/File via isBlob()
200
+ return await AttachmentsService.updateData(existingId, contentId, { file } as any);
201
+ }
202
+ }
203
+ // MockAttachmentRequest types file as string, but getFormData in request.ts handles Blob/File via isBlob()
204
+ const formData = { file, comment, minorEdit, hidden } as any;
205
+ return await AttachmentsService.createAttachments(
206
+ contentId,
207
+ undefined,
208
+ allowDuplicated ? 'true' : undefined,
209
+ undefined,
210
+ formData,
211
+ );
212
+ } finally {
213
+ OpenAPI.HEADERS = prevHeaders;
214
+ }
215
+ }, 'Error uploading attachment');
216
+ }
217
+
218
+ /**
219
+ * Download one or more attachments from a Confluence content entity.
220
+ * @param params.contentId The ID of the content the attachment(s) are on
221
+ * @param params.filename If provided, download only the attachment with this exact filename; otherwise download all attachments on the content
222
+ * @param params.save Whether to write the attachment(s) to the operator-configured download directory
223
+ * @param params.saveName Optional file name (basename only) when saving a single attachment
224
+ * @param params.returnContent Whether/how to embed the bytes inline in the response
225
+ * @param params.maxInlineBytes Inline embedding cap
226
+ * @param params.downloadSide Resolved download gateway (roots, size limit, enabled flag)
227
+ */
228
+ async downloadAttachmentFromContent(params: {
229
+ contentId: string;
230
+ filename?: string;
231
+ save?: boolean;
232
+ saveName?: string;
233
+ returnContent?: AttachmentContentEncoding;
234
+ maxInlineBytes?: number;
235
+ downloadSide: AttachmentGatewaySide;
236
+ }) {
237
+ return handleApiOperation(async () => {
238
+ if (params.save && !params.downloadSide.enabled) {
239
+ throw new Error(
240
+ 'Saving attachments to disk is disabled on this server. Enable it with ' +
241
+ 'CONFLUENCE_ATTACHMENTS_DOWNLOAD_ENABLED and configure a download directory.',
242
+ );
243
+ }
244
+ const list = await AttachmentsService.getAttachments(params.contentId, undefined, params.filename);
245
+ const results = ((list as any)?.results ?? []) as Array<any>;
246
+ if (results.length === 0) {
247
+ throw new Error(
248
+ params.filename
249
+ ? `No attachment named "${params.filename}" found on content ${params.contentId}`
250
+ : `No attachments found on content ${params.contentId}`,
251
+ );
252
+ }
253
+
254
+ const targets = params.filename ? [results[0]] : results;
255
+ const multiple = targets.length > 1;
256
+ const attachments = [];
257
+ for (const attachment of targets) {
258
+ const downloadLink = attachment?._links?.download;
259
+ if (!downloadLink) {
260
+ throw new Error(`Attachment "${attachment?.title ?? params.filename}" has no download link`);
261
+ }
262
+ const name = attachment?.title ?? params.filename ?? 'attachment';
263
+ const mediaType = attachment?.metadata?.mediaType ?? attachment?.extensions?.mediaType;
264
+ let destination: string | undefined;
265
+ if (params.save) {
266
+ const requestedName = multiple ? name : params.saveName ?? name;
267
+ destination = await resolveDownloadDestination({ requestedName, side: params.downloadSide });
268
+ }
269
+ attachments.push(
270
+ await downloadAttachment({
271
+ url: this.buildDownloadUrl(downloadLink),
272
+ token: this.tokenProvider,
273
+ filename: name,
274
+ mediaType,
275
+ options: {
276
+ destination,
277
+ returnContent: params.returnContent,
278
+ maxInlineBytes: params.maxInlineBytes,
279
+ maxDownloadBytes: params.downloadSide.maxBytes,
280
+ },
281
+ }),
282
+ );
283
+ }
284
+
285
+ return { contentId: params.contentId, count: attachments.length, attachments };
286
+ }, 'Error downloading attachment');
287
+ }
288
+
135
289
  /**
136
290
  * Search for spaces by text
137
291
  * @param searchText Text to search for in space names or descriptions
@@ -208,5 +362,25 @@ export const confluenceToolSchemas = {
208
362
  start: z.number().optional().describe("Start index for pagination"),
209
363
  expand: z.string().optional().describe("Comma-separated list of properties to expand"),
210
364
  excerpt: z.enum(['none', 'highlight']).optional().describe("Excerpt mode for search results. Defaults to none.")
365
+ },
366
+ uploadAttachment: {
367
+ contentId: z.string().describe("ID of the Confluence content (page) to attach the file to"),
368
+ sourcePath: z.string().describe("Path to the file to upload, relative to a server-configured attachment upload directory. Absolute paths and '..' segments are rejected; symlinks and non-regular files are refused."),
369
+ filename: z.string().optional().describe("Override for the attachment filename (defaults to the basename of sourcePath)"),
370
+ comment: z.string().optional().describe("Optional comment describing the attachment"),
371
+ minorEdit: z.boolean().optional().describe("If true, no notification email is sent to watchers"),
372
+ hidden: z.boolean().optional().describe("If true, no notification email or activity stream entry is generated"),
373
+ allowDuplicated: z.boolean().optional().describe("Allow upload even if an attachment with the same filename already exists"),
374
+ versionIfExists: z.boolean().optional().describe("If true and an attachment with the same filename already exists, upload as a new version instead of failing")
375
+ },
376
+ downloadAttachment: {
377
+ contentId: z.string().describe("ID of the Confluence content (page) whose attachment(s) to download"),
378
+ filename: z.string().optional().describe("Exact filename of a single attachment to download. If omitted, all attachments on the content are downloaded."),
379
+ returnContent: z.enum(['none', 'base64', 'text']).optional().describe("Whether to embed the file bytes in the response: 'none' (default), 'base64' for binary, or 'text' for UTF-8 text."),
380
+ maxInlineBytes: z.number().optional().describe("Maximum bytes to embed inline when returnContent is base64/text. Larger files are omitted from the inline content. Defaults to 1 MiB.")
381
+ },
382
+ downloadAttachmentSaveFields: {
383
+ save: z.boolean().optional().describe("Save the attachment(s) into the server-configured download directory. Requires disk downloads to be enabled on the server."),
384
+ saveName: z.string().optional().describe("Optional file name (no directories) to use when saving a single attachment; defaults to the attachment's own name. Existing files are never overwritten.")
211
385
  }
212
386
  };
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { connectServer, createMcpServer, formatToolResponse, initializeRuntimeConfig } from '@atlassian-dc-mcp/common';
1
+ import { connectServer, createMcpServer, formatToolResponse, initializeRuntimeConfig, resolveAttachmentGateway } from '@atlassian-dc-mcp/common';
2
2
  import { ConfluenceService, ConfluenceContent, confluenceToolSchemas } from './confluence-service.js';
3
3
  import { shapeConfluenceMutationAck } from './confluence-response-mapper.js';
4
- import { getConfluenceRuntimeConfig, getDefaultPageSize } from './config.js';
4
+ import { CONFLUENCE_PRODUCT, getConfluenceRuntimeConfig, getDefaultPageSize } from './config.js';
5
5
  import { createRequire } from 'node:module';
6
6
 
7
7
  const require = createRequire(import.meta.url);
@@ -141,6 +141,59 @@ server.tool(
141
141
  }
142
142
  );
143
143
 
144
+ const attachmentGateway = resolveAttachmentGateway(CONFLUENCE_PRODUCT);
145
+
146
+ // Filesystem-reading upload is only registered when the operator explicitly enables it.
147
+ if (attachmentGateway.upload.enabled) {
148
+ server.tool(
149
+ "confluence_uploadAttachment",
150
+ `Upload a local file as an attachment to a Confluence content (page) in the ${confluenceInstanceType}. The file must live under the server-configured upload directory; the path is given relative to that directory.`,
151
+ confluenceToolSchemas.uploadAttachment,
152
+ async ({ contentId, sourcePath, filename, comment, minorEdit, hidden, allowDuplicated, versionIfExists }) => {
153
+ const result = await confluenceService.uploadAttachment(
154
+ contentId,
155
+ sourcePath,
156
+ attachmentGateway.upload,
157
+ filename,
158
+ comment,
159
+ minorEdit,
160
+ hidden,
161
+ allowDuplicated,
162
+ versionIfExists,
163
+ );
164
+ return formatToolResponse(result);
165
+ }
166
+ );
167
+ }
168
+
169
+ // Download always returns content inline (no filesystem access). Saving to disk is
170
+ // only offered when the operator enables it, and is confined to the configured directory.
171
+ const downloadSaveEnabled = attachmentGateway.download.enabled;
172
+ server.tool(
173
+ "confluence_downloadAttachment",
174
+ `Download one or more attachments from a Confluence content (page) in the ${confluenceInstanceType}. Returns the file content inline (base64 or text). Useful for inspecting a file or moving it elsewhere (e.g. re-uploading to a Jira issue).${downloadSaveEnabled ? ' Can also save into the server-configured download directory; existing files are never overwritten.' : ' Saving to local disk is disabled on this server.'}`,
175
+ { ...confluenceToolSchemas.downloadAttachment, ...(downloadSaveEnabled ? confluenceToolSchemas.downloadAttachmentSaveFields : {}) },
176
+ async ({ contentId, filename, returnContent, maxInlineBytes, save, saveName }: {
177
+ contentId: string;
178
+ filename?: string;
179
+ returnContent?: 'none' | 'base64' | 'text';
180
+ maxInlineBytes?: number;
181
+ save?: boolean;
182
+ saveName?: string;
183
+ }) => {
184
+ const result = await confluenceService.downloadAttachmentFromContent({
185
+ contentId,
186
+ filename,
187
+ save,
188
+ saveName,
189
+ returnContent,
190
+ maxInlineBytes,
191
+ downloadSide: attachmentGateway.download,
192
+ });
193
+ return formatToolResponse(result);
194
+ }
195
+ );
196
+
144
197
  server.tool('confluence_searchSpace',
145
198
  `Search for spaces in ${confluenceInstanceType}`,
146
199
  confluenceToolSchemas.searchSpaces,