@atlassian-dc-mcp/confluence 0.11.1 → 0.12.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [0.12.0](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.11.2...v0.12.0) (2026-03-18)
7
+
8
+ **Note:** Version bump only for package @atlassian-dc-mcp/confluence
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.11.2](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.11.1...v0.11.2) (2026-03-12)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **confluence:** escape quotes in space search query ([5583bd1](https://github.com/b1ff/atlassian-dc-mcp/commit/5583bd1a5ae4b1e0b5328d14d7ed0a4a5a36154f))
20
+ * **confluence:** robustly escape CQL search text and add test suite ([8104211](https://github.com/b1ff/atlassian-dc-mcp/commit/8104211a695b8411aa9e5f4ba8a977ada58d2157))
21
+
22
+
23
+
24
+
25
+
6
26
  ## [0.11.1](https://github.com/b1ff/atlassian-dc-mcp/compare/v0.11.0...v0.11.1) (2026-03-12)
7
27
 
8
28
  **Note:** Version bump only for package @atlassian-dc-mcp/confluence
package/README.md CHANGED
@@ -60,6 +60,20 @@ Or for development with auto-reload:
60
60
  npm run dev
61
61
  ```
62
62
 
63
+ ## Testing
64
+
65
+ Run the test suite from the package directory:
66
+
67
+ ```
68
+ npm run test
69
+ ```
70
+
71
+ Or from the repository root:
72
+
73
+ ```
74
+ npm run test --workspace=@atlassian-dc-mcp/confluence
75
+ ```
76
+
63
77
  ### Available Tools
64
78
 
65
79
  #### 1. confluence_getContent
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=confluence-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confluence-service.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/confluence-service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,78 @@
1
+ import { ConfluenceService, escapeSearchTextForCql } from '../confluence-service.js';
2
+ import { SearchService } from '../confluence-client/index.js';
3
+ jest.mock('../confluence-client/index.js', () => ({
4
+ ContentResourceService: {},
5
+ SearchService: {
6
+ search1: jest.fn(),
7
+ },
8
+ OpenAPI: {
9
+ BASE: '',
10
+ TOKEN: '',
11
+ VERSION: '',
12
+ },
13
+ }));
14
+ describe('escapeSearchTextForCql', () => {
15
+ it('returns plain text unchanged', () => {
16
+ expect(escapeSearchTextForCql('hello')).toBe('hello');
17
+ expect(escapeSearchTextForCql('space name')).toBe('space name');
18
+ });
19
+ it('escapes double quotes', () => {
20
+ expect(escapeSearchTextForCql('say "hello"')).toBe('say \\"hello\\"');
21
+ });
22
+ it('escapes backslashes first so they cannot escape the following quote', () => {
23
+ expect(escapeSearchTextForCql('\\')).toBe('\\\\');
24
+ expect(escapeSearchTextForCql('path\\to\\space')).toBe('path\\\\to\\\\space');
25
+ });
26
+ it('escapes backslash then quote correctly (order matters)', () => {
27
+ expect(escapeSearchTextForCql('\\"')).toBe('\\\\\\"');
28
+ });
29
+ it('escapes quote then backslash correctly', () => {
30
+ expect(escapeSearchTextForCql('"\\')).toBe('\\"\\\\');
31
+ });
32
+ it('prevents CQL injection via quoted phrase breakout', () => {
33
+ const malicious = '" OR type=page AND text ~ "secret';
34
+ const escaped = escapeSearchTextForCql(malicious);
35
+ expect(escaped).toContain('\\"');
36
+ expect(escaped).not.toBe(malicious);
37
+ });
38
+ it('double-escaping is not idempotent (call once only)', () => {
39
+ const input = 'foo"bar\\baz';
40
+ const once = escapeSearchTextForCql(input);
41
+ const twice = escapeSearchTextForCql(once);
42
+ expect(twice).not.toBe(once);
43
+ expect(twice).toContain('\\\\');
44
+ });
45
+ it('handles empty string', () => {
46
+ expect(escapeSearchTextForCql('')).toBe('');
47
+ });
48
+ });
49
+ describe('ConfluenceService.searchSpaces', () => {
50
+ let service;
51
+ beforeEach(() => {
52
+ service = new ConfluenceService('test-host', 'test-token');
53
+ jest.clearAllMocks();
54
+ });
55
+ it('builds CQL with escaped searchText and calls SearchService', async () => {
56
+ SearchService.search1.mockResolvedValue({ results: [] });
57
+ await service.searchSpaces('my space', 10, 0);
58
+ expect(SearchService.search1).toHaveBeenCalledWith(undefined, undefined, undefined, '10', '0', undefined, 'type=space AND title ~ "my space"');
59
+ });
60
+ it('escapes quotes in searchText in the CQL passed to the API', async () => {
61
+ SearchService.search1.mockResolvedValue({ results: [] });
62
+ await service.searchSpaces('say "hello"', 5);
63
+ expect(SearchService.search1).toHaveBeenCalledWith(undefined, undefined, undefined, '5', undefined, undefined, 'type=space AND title ~ "say \\"hello\\""');
64
+ });
65
+ it('escapes backslashes in searchText in the CQL passed to the API', async () => {
66
+ SearchService.search1.mockResolvedValue({ results: [] });
67
+ await service.searchSpaces('path\\to\\space', 5);
68
+ expect(SearchService.search1).toHaveBeenCalledWith(undefined, undefined, undefined, '5', undefined, undefined, 'type=space AND title ~ "path\\\\to\\\\space"');
69
+ });
70
+ it('forwards API errors via handleApiOperation', async () => {
71
+ const err = new Error('API error');
72
+ SearchService.search1.mockRejectedValue(err);
73
+ const result = await service.searchSpaces('test');
74
+ expect(result.success).toBe(false);
75
+ expect(result.error).toBeDefined();
76
+ });
77
+ });
78
+ //# sourceMappingURL=confluence-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"confluence-service.test.js","sourceRoot":"","sources":["../../src/__tests__/confluence-service.test.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,10 @@
1
1
  import { z } from 'zod';
2
+ /**
3
+ * Escapes user input for safe use inside a CQL quoted string.
4
+ * Escapes backslash first, then double quote, so that neither can break out of the phrase.
5
+ * Only call once per value; double-escaping would over-escape and break the query.
6
+ */
7
+ export declare function escapeSearchTextForCql(searchText: string): string;
2
8
  export interface ConfluenceContent {
3
9
  id?: string;
4
10
  type: string;
@@ -1 +1 @@
1
- {"version":3,"file":"confluence-service.d.ts","sourceRoot":"","sources":["../src/confluence-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,CAAC,EAAE;QACL,OAAO,EAAE;YACP,KAAK,EAAE,MAAM,CAAC;YACd,cAAc,EAAE,SAAS,CAAC;SAC3B,CAAC;KACH,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnC;AAED,qBAAa,iBAAiB;IAC5B;;;;;OAKG;gBACS,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;IAWxE;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;IAQnD;;;;;;OAMG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;;;gBASS,CAAC;mBAIzF,CAAF;gBAA0B,CAAC;gBAChB,CAAA;gBAA0B,CAAC;;;IAVrC;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,iBAAiB;IAI9C;;;;OAIG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB;IAIjE;;;;;;OAMG;IACG,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;;;gBAnBG,CAAC;mBAIzF,CAAF;gBAA0B,CAAC;gBAChB,CAAA;gBAA0B,CAAC;;;IA8BrC,MAAM,CAAC,cAAc,IAAI,MAAM,EAAE;CAelC;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BjC,CAAC"}
1
+ {"version":3,"file":"confluence-service.d.ts","sourceRoot":"","sources":["../src/confluence-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,IAAI,CAAC,EAAE;QACL,OAAO,EAAE;YACP,KAAK,EAAE,MAAM,CAAC;YACd,cAAc,EAAE,SAAS,CAAC;SAC3B,CAAC;KACH,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,SAAS,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnC;AAED,qBAAa,iBAAiB;IAC5B;;;;;OAKG;gBACS,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;IAWxE;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;IAQnD;;;;;;OAMG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;;;gBAC7C,CAAC;mBAA6B,CAAC;gBAA0B,CAAC;gBAA0B,CAAC;gBAA0B,CAAC;;;IAGnJ;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,iBAAiB;IAI9C;;;;OAIG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB;IAIjE;;;;;;OAMG;IACG,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;;;;;;;gBA3BnD,CAAC;mBAA6B,CAAC;gBAA0B,CAAC;gBAA0B,CAAC;gBAA0B,CAAC;;;IA4CnJ,MAAM,CAAC,cAAc,IAAI,MAAM,EAAE;CAelC;AAED,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BjC,CAAC"}
@@ -1,6 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import { ContentResourceService, OpenAPI, SearchService } from './confluence-client/index.js';
3
3
  import { handleApiOperation } from '@atlassian-dc-mcp/common';
4
+ /**
5
+ * Escapes user input for safe use inside a CQL quoted string.
6
+ * Escapes backslash first, then double quote, so that neither can break out of the phrase.
7
+ * Only call once per value; double-escaping would over-escape and break the query.
8
+ */
9
+ export function escapeSearchTextForCql(searchText) {
10
+ return searchText.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
11
+ }
4
12
  export class ConfluenceService {
5
13
  /**
6
14
  * Creates a new ConfluenceService instance
@@ -68,7 +76,8 @@ export class ConfluenceService {
68
76
  async searchSpaces(searchText, limit, start, expand) {
69
77
  // Create a CQL query that searches for spaces
70
78
  // The correct syntax for space search is: type=space AND title ~ "searchText"
71
- const cql = `type=space AND title ~ "${searchText}"`;
79
+ const escapedSearchText = escapeSearchTextForCql(searchText);
80
+ const cql = `type=space AND title ~ "${escapedSearchText}"`;
72
81
  return handleApiOperation(() => SearchService.search1(undefined, expand, undefined, limit?.toString(), start?.toString(), undefined, cql), 'Error searching for spaces');
73
82
  }
74
83
  static validateConfig() {
@@ -110,7 +119,7 @@ export const confluenceToolSchemas = {
110
119
  versionComment: z.string().optional().describe("Comment for this version")
111
120
  },
112
121
  searchSpaces: {
113
- searchText: z.string().describe("Text to search for in Confluence Data Center space names or descriptions"),
122
+ searchText: z.string().describe("Text to search for in Confluence Data Center space names or descriptions. Quotes and backslashes are escaped for CQL; pass the literal search phrase only (do not pre-escape)."),
114
123
  limit: z.number().optional().describe("Maximum number of results to return"),
115
124
  start: z.number().optional().describe("Start index for pagination"),
116
125
  expand: z.string().optional().describe("Comma-separated list of properties to expand")
@@ -1 +1 @@
1
- {"version":3,"file":"confluence-service.js","sourceRoot":"","sources":["../src/confluence-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,sBAAsB,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAsB9D,MAAM,OAAO,iBAAiB;IAC5B;;;;;OAKG;IACH,YAAY,IAAwB,EAAE,KAAa,EAAE,UAAmB;QACtE,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,GAAG,UAAU,CAAC;QAC5B,CAAC;aAAM,IAAI,IAAI,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,GAAG,WAAW,IAAI,EAAE,CAAC;QACnC,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;QACtB,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC;IAC1B,CAAC;IACD;;;;OAIG;IACH,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,MAAe;QACjD,MAAM,WAAW,GAAG,MAAM,IAAI,cAAc,CAAC;QAC7C,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC;YAC5D,CAAC,CAAC,GAAG,MAAM,eAAe;YAC1B,CAAC,CAAC,WAAW,CAAC;QAChB,OAAO,kBAAkB,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,cAAc,CAAC,SAAS,EAAE,WAAW,CAAC,EAAE,uBAAuB,CAAC,CAAC;IAC1H,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,aAAa,CAAC,GAAW,EAAE,KAAc,EAAE,KAAc,EAAE,MAAe;QAC9E,OAAO,kBAAkB,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,GAAG,CAAC,EAAE,6BAA6B,CAAC,CAAC;IAC5K,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,aAAa,CAAC,OAA0B;QAC5C,OAAO,kBAAkB,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,wBAAwB,CAAC,CAAC;IAC3G,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,aAAa,CAAC,SAAiB,EAAE,OAA0B;QAC/D,OAAO,kBAAkB,CAAC,GAAG,EAAE,CAAC,sBAAsB,CAAC,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,wBAAwB,CAAC,CAAC;IAChH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAAC,UAAkB,EAAE,KAAc,EAAE,KAAc,EAAE,MAAe;QACpF,8CAA8C;QAC9C,8EAA8E;QAC9E,MAAM,GAAG,GAAG,2BAA2B,UAAU,GAAG,CAAC;QAErD,OAAO,kBAAkB,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CACnD,SAAS,EACT,MAAM,EACN,SAAS,EACT,KAAK,EAAE,QAAQ,EAAE,EACjB,KAAK,EAAE,QAAQ,EAAE,EACjB,SAAS,EACT,GAAG,CACJ,EAAE,4BAA4B,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,CAAC,cAAc;QACnB,MAAM,WAAW,GAAa,EAAE,CAAC;QAEjC,+BAA+B;QAC/B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,EAAE,CAAC;YACtC,WAAW,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;QAC3C,CAAC;QAED,iEAAiE;QACjE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,EAAE,CAAC;YAC1E,WAAW,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;QAClE,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,UAAU,EAAE;QACV,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mCAAmC,CAAC;QACnE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;KACvF;IACD,aAAa,EAAE;QACb,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0EAA0E,CAAC;QACpG,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;QAC5E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4BAA4B,CAAC;QACnE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;KACvF;IACD,aAAa,EAAE;QACb,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QAClD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,yCAAyC,CAAC;QACxE,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,oCAAoC,CAAC;QAC/E,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,4EAA4E,CAAC;QAC1G,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,kDAAkD,CAAC;KAC7F;IACD,aAAa,EAAE;QACb,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,6BAA6B,CAAC;QAC7D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0BAA0B,CAAC;QACjE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,uEAAuE,CAAC;QAChH,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0CAA0C,CAAC;QACxE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,0BAA0B,CAAC;KAC3E;IACD,YAAY,EAAE;QACZ,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0EAA0E,CAAC;QAC3G,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,qCAAqC,CAAC;QAC5E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,4BAA4B,CAAC;QACnE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,8CAA8C,CAAC;KACvF;CACF,CAAC"}
1
+ {"version":3,"file":"confluence-service.js","sourceRoot":"","sources":["../src/confluence-service.ts"],"names":[],"mappings":"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"}
package/jest.config.js ADDED
@@ -0,0 +1,23 @@
1
+ export default {
2
+ preset: 'ts-jest/presets/default-esm',
3
+ extensionsToTreatAsEsm: ['.ts'],
4
+ moduleNameMapper: {
5
+ '^(\\.{1,2}/.*)\\.js$': '$1',
6
+ '^@atlassian-dc-mcp/common$': '<rootDir>/../common/src/index.ts',
7
+ },
8
+ transform: {
9
+ '^.+\\.ts$': ['ts-jest', {
10
+ useESM: true,
11
+ }],
12
+ },
13
+ transformIgnorePatterns: [
14
+ 'node_modules/(?!(@atlassian-dc-mcp)/)',
15
+ ],
16
+ testEnvironment: 'node',
17
+ testMatch: ['**/__tests__/**/*.test.ts'],
18
+ collectCoverageFrom: [
19
+ 'src/**/*.ts',
20
+ '!src/**/*.d.ts',
21
+ '!src/**/index.ts',
22
+ ],
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlassian-dc-mcp/confluence",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "main": "build/index.js",
5
5
  "type": "module",
6
6
  "bin": "./bin/run.js",
@@ -14,7 +14,7 @@
14
14
  "test": "jest --passWithNoTests"
15
15
  },
16
16
  "dependencies": {
17
- "@atlassian-dc-mcp/common": "^0.11.1",
17
+ "@atlassian-dc-mcp/common": "^0.12.0",
18
18
  "@modelcontextprotocol/sdk": "^1.27.1",
19
19
  "dotenv": "^16.4.7",
20
20
  "node-fetch": "^3.3.2",
@@ -29,5 +29,5 @@
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
- "gitHead": "d15297baa77ff7da68d3e3389f01e8d1d6b464dc"
32
+ "gitHead": "aa5b3696f257a8fccfc1930d2a46c17ecd695cdc"
33
33
  }
@@ -0,0 +1,124 @@
1
+ import { ConfluenceService, escapeSearchTextForCql } from '../confluence-service.js';
2
+ import { SearchService } from '../confluence-client/index.js';
3
+
4
+ jest.mock('../confluence-client/index.js', () => ({
5
+ ContentResourceService: {},
6
+ SearchService: {
7
+ search1: jest.fn(),
8
+ },
9
+ OpenAPI: {
10
+ BASE: '',
11
+ TOKEN: '',
12
+ VERSION: '',
13
+ },
14
+ }));
15
+
16
+ describe('escapeSearchTextForCql', () => {
17
+ it('returns plain text unchanged', () => {
18
+ expect(escapeSearchTextForCql('hello')).toBe('hello');
19
+ expect(escapeSearchTextForCql('space name')).toBe('space name');
20
+ });
21
+
22
+ it('escapes double quotes', () => {
23
+ expect(escapeSearchTextForCql('say "hello"')).toBe('say \\"hello\\"');
24
+ });
25
+
26
+ it('escapes backslashes first so they cannot escape the following quote', () => {
27
+ expect(escapeSearchTextForCql('\\')).toBe('\\\\');
28
+ expect(escapeSearchTextForCql('path\\to\\space')).toBe('path\\\\to\\\\space');
29
+ });
30
+
31
+ it('escapes backslash then quote correctly (order matters)', () => {
32
+ expect(escapeSearchTextForCql('\\"')).toBe('\\\\\\"');
33
+ });
34
+
35
+ it('escapes quote then backslash correctly', () => {
36
+ expect(escapeSearchTextForCql('"\\')).toBe('\\"\\\\');
37
+ });
38
+
39
+ it('prevents CQL injection via quoted phrase breakout', () => {
40
+ const malicious = '" OR type=page AND text ~ "secret';
41
+ const escaped = escapeSearchTextForCql(malicious);
42
+ expect(escaped).toContain('\\"');
43
+ expect(escaped).not.toBe(malicious);
44
+ });
45
+
46
+ it('double-escaping is not idempotent (call once only)', () => {
47
+ const input = 'foo"bar\\baz';
48
+ const once = escapeSearchTextForCql(input);
49
+ const twice = escapeSearchTextForCql(once);
50
+ expect(twice).not.toBe(once);
51
+ expect(twice).toContain('\\\\');
52
+ });
53
+
54
+ it('handles empty string', () => {
55
+ expect(escapeSearchTextForCql('')).toBe('');
56
+ });
57
+ });
58
+
59
+ describe('ConfluenceService.searchSpaces', () => {
60
+ let service: ConfluenceService;
61
+
62
+ beforeEach(() => {
63
+ service = new ConfluenceService('test-host', 'test-token');
64
+ jest.clearAllMocks();
65
+ });
66
+
67
+ it('builds CQL with escaped searchText and calls SearchService', async () => {
68
+ (SearchService.search1 as jest.Mock).mockResolvedValue({ results: [] });
69
+
70
+ await service.searchSpaces('my space', 10, 0);
71
+
72
+ expect(SearchService.search1).toHaveBeenCalledWith(
73
+ undefined,
74
+ undefined,
75
+ undefined,
76
+ '10',
77
+ '0',
78
+ undefined,
79
+ 'type=space AND title ~ "my space"'
80
+ );
81
+ });
82
+
83
+ it('escapes quotes in searchText in the CQL passed to the API', async () => {
84
+ (SearchService.search1 as jest.Mock).mockResolvedValue({ results: [] });
85
+
86
+ await service.searchSpaces('say "hello"', 5);
87
+
88
+ expect(SearchService.search1).toHaveBeenCalledWith(
89
+ undefined,
90
+ undefined,
91
+ undefined,
92
+ '5',
93
+ undefined,
94
+ undefined,
95
+ 'type=space AND title ~ "say \\"hello\\""'
96
+ );
97
+ });
98
+
99
+ it('escapes backslashes in searchText in the CQL passed to the API', async () => {
100
+ (SearchService.search1 as jest.Mock).mockResolvedValue({ results: [] });
101
+
102
+ await service.searchSpaces('path\\to\\space', 5);
103
+
104
+ expect(SearchService.search1).toHaveBeenCalledWith(
105
+ undefined,
106
+ undefined,
107
+ undefined,
108
+ '5',
109
+ undefined,
110
+ undefined,
111
+ 'type=space AND title ~ "path\\\\to\\\\space"'
112
+ );
113
+ });
114
+
115
+ it('forwards API errors via handleApiOperation', async () => {
116
+ const err = new Error('API error');
117
+ (SearchService.search1 as jest.Mock).mockRejectedValue(err);
118
+
119
+ const result = await service.searchSpaces('test');
120
+
121
+ expect(result.success).toBe(false);
122
+ expect(result.error).toBeDefined();
123
+ });
124
+ });
@@ -2,6 +2,15 @@ import { z } from 'zod';
2
2
  import { ContentResourceService, OpenAPI, SearchService } from './confluence-client/index.js';
3
3
  import { handleApiOperation } from '@atlassian-dc-mcp/common';
4
4
 
5
+ /**
6
+ * Escapes user input for safe use inside a CQL quoted string.
7
+ * Escapes backslash first, then double quote, so that neither can break out of the phrase.
8
+ * Only call once per value; double-escaping would over-escape and break the query.
9
+ */
10
+ export function escapeSearchTextForCql(searchText: string): string {
11
+ return searchText.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
12
+ }
13
+
5
14
  export interface ConfluenceContent {
6
15
  id?: string;
7
16
  type: string;
@@ -91,7 +100,8 @@ export class ConfluenceService {
91
100
  async searchSpaces(searchText: string, limit?: number, start?: number, expand?: string) {
92
101
  // Create a CQL query that searches for spaces
93
102
  // The correct syntax for space search is: type=space AND title ~ "searchText"
94
- const cql = `type=space AND title ~ "${searchText}"`;
103
+ const escapedSearchText = escapeSearchTextForCql(searchText);
104
+ const cql = `type=space AND title ~ "${escapedSearchText}"`;
95
105
 
96
106
  return handleApiOperation(() => SearchService.search1(
97
107
  undefined,
@@ -147,7 +157,7 @@ export const confluenceToolSchemas = {
147
157
  versionComment: z.string().optional().describe("Comment for this version")
148
158
  },
149
159
  searchSpaces: {
150
- searchText: z.string().describe("Text to search for in Confluence Data Center space names or descriptions"),
160
+ searchText: z.string().describe("Text to search for in Confluence Data Center space names or descriptions. Quotes and backslashes are escaped for CQL; pass the literal search phrase only (do not pre-escape)."),
151
161
  limit: z.number().optional().describe("Maximum number of results to return"),
152
162
  start: z.number().optional().describe("Start index for pagination"),
153
163
  expand: z.string().optional().describe("Comma-separated list of properties to expand")