@atlassian-dc-mcp/confluence 0.12.2 → 0.13.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 +12 -0
- package/README.md +41 -2
- package/build/__tests__/config.test.d.ts +2 -0
- package/build/__tests__/config.test.d.ts.map +1 -0
- package/build/__tests__/config.test.js +49 -0
- package/build/__tests__/config.test.js.map +1 -0
- package/build/__tests__/confluence-response-mapper.test.d.ts +2 -0
- package/build/__tests__/confluence-response-mapper.test.d.ts.map +1 -0
- package/build/__tests__/confluence-response-mapper.test.js +24 -0
- package/build/__tests__/confluence-response-mapper.test.js.map +1 -0
- package/build/__tests__/confluence-service.test.js +125 -5
- package/build/__tests__/confluence-service.test.js.map +1 -1
- package/build/config.d.ts +4 -0
- package/build/config.d.ts.map +1 -0
- package/build/config.js +11 -0
- package/build/config.js.map +1 -0
- package/build/confluence-response-mapper.d.ts +13 -0
- package/build/confluence-response-mapper.d.ts.map +1 -0
- package/build/confluence-response-mapper.js +87 -0
- package/build/confluence-response-mapper.js.map +1 -0
- package/build/confluence-service.d.ts +18 -4
- package/build/confluence-service.d.ts.map +1 -1
- package/build/confluence-service.js +42 -22
- package/build/confluence-service.js.map +1 -1
- package/build/index.js +27 -14
- package/build/index.js.map +1 -1
- package/package.json +3 -3
- package/server.json +12 -5
- package/src/__tests__/config.test.ts +64 -0
- package/src/__tests__/confluence-response-mapper.test.ts +26 -0
- package/src/__tests__/confluence-service.test.ts +161 -5
- package/src/config.ts +13 -0
- package/src/confluence-response-mapper.ts +104 -0
- package/src/confluence-service.ts +69 -26
- package/src/index.ts +31 -16
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
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
|
+
import { getDefaultPageSize, getMissingConfig } from './config.js';
|
|
5
|
+
import { shapeConfluenceContent } from './confluence-response-mapper.js';
|
|
4
6
|
/**
|
|
5
7
|
* Escapes user input for safe use inside a CQL quoted string.
|
|
6
8
|
* Escapes backslash first, then double quote, so that neither can break out of the phrase.
|
|
@@ -9,14 +11,24 @@ import { handleApiOperation } from '@atlassian-dc-mcp/common';
|
|
|
9
11
|
export function escapeSearchTextForCql(searchText) {
|
|
10
12
|
return searchText.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
11
13
|
}
|
|
14
|
+
function resolveToken(token, missingTokenMessage) {
|
|
15
|
+
return async () => {
|
|
16
|
+
const resolvedToken = typeof token === 'function' ? token() : token;
|
|
17
|
+
if (!resolvedToken) {
|
|
18
|
+
throw new Error(missingTokenMessage);
|
|
19
|
+
}
|
|
20
|
+
return resolvedToken;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
12
23
|
export class ConfluenceService {
|
|
24
|
+
getPageSize;
|
|
13
25
|
/**
|
|
14
26
|
* Creates a new ConfluenceService instance
|
|
15
27
|
* @param host The hostname of the Confluence server (e.g., "host.com")
|
|
16
28
|
* @param token The API token for authentication
|
|
17
29
|
* @param fullApiUrl Optional full API URL (e.g., "https://host.com/wiki/"). If provided, host and apiBasePath are ignored.
|
|
18
30
|
*/
|
|
19
|
-
constructor(host, token, fullApiUrl) {
|
|
31
|
+
constructor(host, token, fullApiUrl, getPageSize = getDefaultPageSize) {
|
|
20
32
|
if (fullApiUrl) {
|
|
21
33
|
OpenAPI.BASE = fullApiUrl;
|
|
22
34
|
}
|
|
@@ -26,21 +38,32 @@ export class ConfluenceService {
|
|
|
26
38
|
else {
|
|
27
39
|
throw new Error('Either host or fullApiUrl must be provided');
|
|
28
40
|
}
|
|
29
|
-
OpenAPI.TOKEN = token;
|
|
41
|
+
OpenAPI.TOKEN = resolveToken(token, 'Missing required environment variable: CONFLUENCE_API_TOKEN');
|
|
30
42
|
OpenAPI.VERSION = '1.0';
|
|
43
|
+
this.getPageSize = getPageSize;
|
|
31
44
|
}
|
|
32
45
|
/**
|
|
33
46
|
* Get a Confluence page by ID
|
|
34
47
|
* @param contentId The ID of the page to retrieve
|
|
35
48
|
* @param expand Optional comma-separated list of properties to expand
|
|
36
49
|
*/
|
|
37
|
-
async
|
|
50
|
+
async getContentRaw(contentId, expand) {
|
|
38
51
|
const expandValue = expand || 'body.storage';
|
|
39
52
|
const finalExpand = expand && !expand.includes('body.storage')
|
|
40
53
|
? `${expand},body.storage`
|
|
41
54
|
: expandValue;
|
|
42
55
|
return handleApiOperation(() => ContentResourceService.getContentById(contentId, finalExpand), 'Error getting content');
|
|
43
56
|
}
|
|
57
|
+
async getContent(contentId, expand, bodyMode = 'storage', maxBodyChars) {
|
|
58
|
+
const result = await this.getContentRaw(contentId, expand);
|
|
59
|
+
if (result.success && result.data) {
|
|
60
|
+
return {
|
|
61
|
+
...result,
|
|
62
|
+
data: shapeConfluenceContent(result.data, bodyMode, maxBodyChars),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
44
67
|
/**
|
|
45
68
|
* Search for content in Confluence using CQL
|
|
46
69
|
* @param cql Confluence Query Language string
|
|
@@ -48,8 +71,8 @@ export class ConfluenceService {
|
|
|
48
71
|
* @param start Start index for pagination
|
|
49
72
|
* @param expand Optional comma-separated list of properties to expand
|
|
50
73
|
*/
|
|
51
|
-
async searchContent(cql, limit, start, expand) {
|
|
52
|
-
return handleApiOperation(() => SearchService.search1(undefined, expand, undefined, limit
|
|
74
|
+
async searchContent(cql, limit, start, expand, excerpt = 'none') {
|
|
75
|
+
return handleApiOperation(() => SearchService.search1(undefined, expand, undefined, (limit ?? this.getPageSize()).toString(), start?.toString(), excerpt, cql), 'Error searching for content');
|
|
53
76
|
}
|
|
54
77
|
/**
|
|
55
78
|
* Create a new page in Confluence
|
|
@@ -73,56 +96,53 @@ export class ConfluenceService {
|
|
|
73
96
|
* @param start Start index for pagination
|
|
74
97
|
* @param expand Optional comma-separated list of properties to expand
|
|
75
98
|
*/
|
|
76
|
-
async searchSpaces(searchText, limit, start, expand) {
|
|
99
|
+
async searchSpaces(searchText, limit, start, expand, excerpt = 'none') {
|
|
77
100
|
// Create a CQL query that searches for spaces
|
|
78
101
|
// The correct syntax for space search is: type=space AND title ~ "searchText"
|
|
79
102
|
const escapedSearchText = escapeSearchTextForCql(searchText);
|
|
80
103
|
const cql = `type=space AND title ~ "${escapedSearchText}"`;
|
|
81
|
-
return handleApiOperation(() => SearchService.search1(undefined, expand, undefined, limit
|
|
104
|
+
return handleApiOperation(() => SearchService.search1(undefined, expand, undefined, (limit ?? this.getPageSize()).toString(), start?.toString(), excerpt, cql), 'Error searching for spaces');
|
|
82
105
|
}
|
|
83
106
|
static validateConfig() {
|
|
84
|
-
|
|
85
|
-
// API token is always required
|
|
86
|
-
if (!process.env.CONFLUENCE_API_TOKEN) {
|
|
87
|
-
missingVars.push('CONFLUENCE_API_TOKEN');
|
|
88
|
-
}
|
|
89
|
-
// Either CONFLUENCE_HOST or CONFLUENCE_API_BASE_PATH must be set
|
|
90
|
-
if (!process.env.CONFLUENCE_HOST && !process.env.CONFLUENCE_API_BASE_PATH) {
|
|
91
|
-
missingVars.push('CONFLUENCE_HOST or CONFLUENCE_API_BASE_PATH');
|
|
92
|
-
}
|
|
93
|
-
return missingVars;
|
|
107
|
+
return getMissingConfig();
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
110
|
export const confluenceToolSchemas = {
|
|
97
111
|
getContent: {
|
|
98
112
|
contentId: z.string().describe("Confluence Data Center content ID"),
|
|
99
|
-
expand: z.string().optional().describe("Comma-separated list of properties to expand")
|
|
113
|
+
expand: z.string().optional().describe("Comma-separated list of properties to expand"),
|
|
114
|
+
bodyMode: z.enum(['storage', 'text', 'none']).optional().describe("How to return the page body. Defaults to storage for backward compatibility."),
|
|
115
|
+
maxBodyChars: z.number().optional().describe("Maximum number of characters to keep when bodyMode is text")
|
|
100
116
|
},
|
|
101
117
|
searchContent: {
|
|
102
118
|
cql: z.string().describe("Confluence Query Language (CQL) search string for Confluence Data Center"),
|
|
103
119
|
limit: z.number().optional().describe("Maximum number of results to return"),
|
|
104
120
|
start: z.number().optional().describe("Start index for pagination"),
|
|
105
|
-
expand: z.string().optional().describe("Comma-separated list of properties to expand")
|
|
121
|
+
expand: z.string().optional().describe("Comma-separated list of properties to expand"),
|
|
122
|
+
excerpt: z.enum(['none', 'highlight']).optional().describe("Excerpt mode for search results. Defaults to none.")
|
|
106
123
|
},
|
|
107
124
|
createContent: {
|
|
108
125
|
title: z.string().describe("Title of the content"),
|
|
109
126
|
spaceKey: z.string().describe("Space key where content will be created"),
|
|
110
127
|
type: z.string().default("page").describe("Content type (page, blogpost, etc)"),
|
|
111
128
|
content: z.string().describe("Content body in Confluence Data Center \"storage\" format (confluence XML)"),
|
|
112
|
-
parentId: z.string().optional().describe("ID of the parent page (if creating a child page)")
|
|
129
|
+
parentId: z.string().optional().describe("ID of the parent page (if creating a child page)"),
|
|
130
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
113
131
|
},
|
|
114
132
|
updateContent: {
|
|
115
133
|
contentId: z.string().describe("ID of the content to update"),
|
|
116
134
|
title: z.string().optional().describe("New title of the content"),
|
|
117
135
|
content: z.string().optional().describe("New content body in Confluence Data Center storage format (XML-based)"),
|
|
118
136
|
version: z.number().describe("New version number (must be incremented)"),
|
|
119
|
-
versionComment: z.string().optional().describe("Comment for this version")
|
|
137
|
+
versionComment: z.string().optional().describe("Comment for this version"),
|
|
138
|
+
output: z.enum(['ack', 'full']).optional().describe("Return a compact acknowledgement or the full API response. Defaults to ack.")
|
|
120
139
|
},
|
|
121
140
|
searchSpaces: {
|
|
122
141
|
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)."),
|
|
123
142
|
limit: z.number().optional().describe("Maximum number of results to return"),
|
|
124
143
|
start: z.number().optional().describe("Start index for pagination"),
|
|
125
|
-
expand: z.string().optional().describe("Comma-separated list of properties to expand")
|
|
144
|
+
expand: z.string().optional().describe("Comma-separated list of properties to expand"),
|
|
145
|
+
excerpt: z.enum(['none', 'highlight']).optional().describe("Excerpt mode for search results. Defaults to none.")
|
|
126
146
|
}
|
|
127
147
|
};
|
|
128
148
|
//# sourceMappingURL=confluence-service.js.map
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"confluence-service.js","sourceRoot":"","sources":["../src/confluence-service.ts"],"names":[],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { connectServer, createMcpServer, formatToolResponse } from '@atlassian-dc-mcp/common';
|
|
1
|
+
import { connectServer, createMcpServer, formatToolResponse, initializeRuntimeConfig } from '@atlassian-dc-mcp/common';
|
|
2
2
|
import { ConfluenceService, confluenceToolSchemas } from './confluence-service.js';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { shapeConfluenceMutationAck } from './confluence-response-mapper.js';
|
|
4
|
+
import { getConfluenceRuntimeConfig, getDefaultPageSize } from './config.js';
|
|
5
|
+
initializeRuntimeConfig();
|
|
6
6
|
// Validate required environment variables
|
|
7
7
|
const missingEnvVars = ConfluenceService.validateConfig();
|
|
8
8
|
if (missingEnvVars.length > 0) {
|
|
9
9
|
throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
|
|
10
10
|
}
|
|
11
11
|
// Initialize Confluence service
|
|
12
|
-
const
|
|
12
|
+
const confluenceConfig = getConfluenceRuntimeConfig();
|
|
13
|
+
const confluenceService = new ConfluenceService(confluenceConfig.host, () => getConfluenceRuntimeConfig().token, confluenceConfig.apiBasePath, getDefaultPageSize);
|
|
13
14
|
// Define Confluence instance type
|
|
14
15
|
const confluenceInstanceType = "Confluence Data Center edition instance";
|
|
15
16
|
// Initialize MCP server
|
|
@@ -18,15 +19,15 @@ const server = createMcpServer({
|
|
|
18
19
|
version: "1.0.0"
|
|
19
20
|
});
|
|
20
21
|
// Add Confluence content tools
|
|
21
|
-
server.tool("confluence_getContent", `Get Confluence content by ID from the ${confluenceInstanceType}`, confluenceToolSchemas.getContent, async ({ contentId, expand }) => {
|
|
22
|
-
const result = await confluenceService.getContent(contentId, expand);
|
|
22
|
+
server.tool("confluence_getContent", `Get Confluence content by ID from the ${confluenceInstanceType}`, confluenceToolSchemas.getContent, async ({ contentId, expand, bodyMode, maxBodyChars }) => {
|
|
23
|
+
const result = await confluenceService.getContent(contentId, expand, bodyMode, maxBodyChars);
|
|
23
24
|
return formatToolResponse(result);
|
|
24
25
|
});
|
|
25
|
-
server.tool("confluence_searchContent", `Search for content in ${confluenceInstanceType} using CQL`, confluenceToolSchemas.searchContent, async ({ cql, limit, start, expand }) => {
|
|
26
|
-
const result = await confluenceService.searchContent(cql, limit, start, expand);
|
|
26
|
+
server.tool("confluence_searchContent", `Search for content in ${confluenceInstanceType} using CQL`, confluenceToolSchemas.searchContent, async ({ cql, limit, start, expand, excerpt }) => {
|
|
27
|
+
const result = await confluenceService.searchContent(cql, limit, start, expand, excerpt);
|
|
27
28
|
return formatToolResponse(result);
|
|
28
29
|
});
|
|
29
|
-
server.tool("confluence_createContent", `Create new content in ${confluenceInstanceType}`, confluenceToolSchemas.createContent, async ({ title, spaceKey, type, content, parentId }) => {
|
|
30
|
+
server.tool("confluence_createContent", `Create new content in ${confluenceInstanceType}`, confluenceToolSchemas.createContent, async ({ title, spaceKey, type, content, parentId, output }) => {
|
|
30
31
|
const contentObj = {
|
|
31
32
|
type: type || 'page',
|
|
32
33
|
title,
|
|
@@ -43,11 +44,17 @@ server.tool("confluence_createContent", `Create new content in ${confluenceInsta
|
|
|
43
44
|
contentObj.ancestors = [{ id: parentId }];
|
|
44
45
|
}
|
|
45
46
|
const result = await confluenceService.createContent(contentObj);
|
|
47
|
+
if (result.success && result.data && output !== 'full') {
|
|
48
|
+
return formatToolResponse({
|
|
49
|
+
...result,
|
|
50
|
+
data: shapeConfluenceMutationAck(result.data),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
46
53
|
return formatToolResponse(result);
|
|
47
54
|
});
|
|
48
|
-
server.tool("confluence_updateContent", `Update existing content in ${confluenceInstanceType}`, confluenceToolSchemas.updateContent, async ({ contentId, title, content, version, versionComment }) => {
|
|
55
|
+
server.tool("confluence_updateContent", `Update existing content in ${confluenceInstanceType}`, confluenceToolSchemas.updateContent, async ({ contentId, title, content, version, versionComment, output }) => {
|
|
49
56
|
// First get the current content to build upon
|
|
50
|
-
const currentContent = await confluenceService.
|
|
57
|
+
const currentContent = await confluenceService.getContentRaw(contentId);
|
|
51
58
|
if (!currentContent.success || !currentContent.data) {
|
|
52
59
|
return formatToolResponse({
|
|
53
60
|
success: false,
|
|
@@ -76,10 +83,16 @@ server.tool("confluence_updateContent", `Update existing content in ${confluence
|
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
85
|
const result = await confluenceService.updateContent(contentId, updateObj);
|
|
86
|
+
if (result.success && result.data && output !== 'full') {
|
|
87
|
+
return formatToolResponse({
|
|
88
|
+
...result,
|
|
89
|
+
data: shapeConfluenceMutationAck(result.data),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
79
92
|
return formatToolResponse(result);
|
|
80
93
|
});
|
|
81
|
-
server.tool('confluence_searchSpace', `Search for spaces in ${confluenceInstanceType}`, confluenceToolSchemas.searchSpaces, async ({ searchText, limit, start, expand }) => {
|
|
82
|
-
const result = await confluenceService.searchSpaces(searchText, limit, start, expand);
|
|
94
|
+
server.tool('confluence_searchSpace', `Search for spaces in ${confluenceInstanceType}`, confluenceToolSchemas.searchSpaces, async ({ searchText, limit, start, expand, excerpt }) => {
|
|
95
|
+
const result = await confluenceService.searchSpaces(searchText, limit, start, expand, excerpt);
|
|
83
96
|
return formatToolResponse(result);
|
|
84
97
|
});
|
|
85
98
|
await connectServer(server);
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlassian-dc-mcp/confluence",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.
|
|
17
|
+
"@atlassian-dc-mcp/common": "^0.13.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": "
|
|
32
|
+
"gitHead": "2f5845bf1c5837d547bcc957dcf01c4f9865c83a"
|
|
33
33
|
}
|
package/server.json
CHANGED
|
@@ -18,22 +18,29 @@
|
|
|
18
18
|
},
|
|
19
19
|
"environmentVariables": [
|
|
20
20
|
{
|
|
21
|
-
"description": "
|
|
21
|
+
"description": "Absolute path to a shared dotenv-style config file. When set, values are read from this file before direct environment variable overrides are applied.",
|
|
22
|
+
"isRequired": false,
|
|
23
|
+
"format": "string",
|
|
24
|
+
"isSecret": false,
|
|
25
|
+
"name": "ATLASSIAN_DC_MCP_CONFIG_FILE"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"description": "Confluence host domain (e.g. your-instance.atlassian.net). Required unless provided through ATLASSIAN_DC_MCP_CONFIG_FILE or CONFLUENCE_API_BASE_PATH.",
|
|
22
29
|
"isRequired": false,
|
|
23
30
|
"format": "string",
|
|
24
31
|
"isSecret": false,
|
|
25
32
|
"name": "CONFLUENCE_HOST"
|
|
26
33
|
},
|
|
27
34
|
{
|
|
28
|
-
"description": "Confluence API base path (alternative to CONFLUENCE_HOST)",
|
|
35
|
+
"description": "Confluence API base path (alternative to CONFLUENCE_HOST). Required unless provided through ATLASSIAN_DC_MCP_CONFIG_FILE or CONFLUENCE_HOST.",
|
|
29
36
|
"isRequired": false,
|
|
30
37
|
"format": "string",
|
|
31
38
|
"isSecret": false,
|
|
32
39
|
"name": "CONFLUENCE_API_BASE_PATH"
|
|
33
40
|
},
|
|
34
41
|
{
|
|
35
|
-
"description": "Confluence Personal Access Token or API token",
|
|
36
|
-
"isRequired":
|
|
42
|
+
"description": "Confluence Personal Access Token or API token. Required unless provided through ATLASSIAN_DC_MCP_CONFIG_FILE.",
|
|
43
|
+
"isRequired": false,
|
|
37
44
|
"format": "string",
|
|
38
45
|
"isSecret": true,
|
|
39
46
|
"name": "CONFLUENCE_API_TOKEN"
|
|
@@ -41,4 +48,4 @@
|
|
|
41
48
|
]
|
|
42
49
|
}
|
|
43
50
|
]
|
|
44
|
-
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
describe('Confluence config', () => {
|
|
6
|
+
const originalEnv = process.env;
|
|
7
|
+
const originalCwd = process.cwd();
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.resetModules();
|
|
12
|
+
process.env = { ...originalEnv };
|
|
13
|
+
delete process.env.ATLASSIAN_DC_MCP_CONFIG_FILE;
|
|
14
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'confluence-config-'));
|
|
15
|
+
process.chdir(tempDir);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
20
|
+
process.chdir(originalCwd);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterAll(() => {
|
|
24
|
+
process.env = originalEnv;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('uses the configured page size when the env var is a positive integer', async () => {
|
|
28
|
+
process.env.CONFLUENCE_DEFAULT_PAGE_SIZE = '40';
|
|
29
|
+
|
|
30
|
+
const { getDefaultPageSize } = await import('../config.js');
|
|
31
|
+
|
|
32
|
+
expect(getDefaultPageSize()).toBe(40);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('falls back to 25 when the env var is invalid', async () => {
|
|
36
|
+
process.env.CONFLUENCE_DEFAULT_PAGE_SIZE = '0';
|
|
37
|
+
|
|
38
|
+
const { getDefaultPageSize } = await import('../config.js');
|
|
39
|
+
|
|
40
|
+
expect(getDefaultPageSize()).toBe(25);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('reads the page size from the shared config file', async () => {
|
|
44
|
+
const sharedConfigPath = path.join(tempDir, 'shared.env');
|
|
45
|
+
fs.writeFileSync(sharedConfigPath, 'CONFLUENCE_HOST=file-host\nCONFLUENCE_API_TOKEN=file-token\nCONFLUENCE_DEFAULT_PAGE_SIZE=35\n');
|
|
46
|
+
process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
|
|
47
|
+
|
|
48
|
+
const { getConfluenceRuntimeConfig, getDefaultPageSize } = await import('../config.js');
|
|
49
|
+
|
|
50
|
+
expect(getDefaultPageSize()).toBe(35);
|
|
51
|
+
expect(getConfluenceRuntimeConfig().token).toBe('file-token');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('keeps env values higher priority than the shared config file', async () => {
|
|
55
|
+
const sharedConfigPath = path.join(tempDir, 'shared.env');
|
|
56
|
+
fs.writeFileSync(sharedConfigPath, 'CONFLUENCE_HOST=file-host\nCONFLUENCE_API_TOKEN=file-token\nCONFLUENCE_DEFAULT_PAGE_SIZE=35\n');
|
|
57
|
+
process.env.ATLASSIAN_DC_MCP_CONFIG_FILE = sharedConfigPath;
|
|
58
|
+
process.env.CONFLUENCE_DEFAULT_PAGE_SIZE = '45';
|
|
59
|
+
|
|
60
|
+
const { getDefaultPageSize } = await import('../config.js');
|
|
61
|
+
|
|
62
|
+
expect(getDefaultPageSize()).toBe(45);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { shapeConfluenceMutationAck } from '../confluence-response-mapper.js';
|
|
2
|
+
|
|
3
|
+
describe('shapeConfluenceMutationAck', () => {
|
|
4
|
+
it('returns a compact acknowledgement with a resolved content URL', () => {
|
|
5
|
+
expect(
|
|
6
|
+
shapeConfluenceMutationAck({
|
|
7
|
+
id: '123',
|
|
8
|
+
type: 'page',
|
|
9
|
+
title: 'Test page',
|
|
10
|
+
space: { key: 'DOCS' },
|
|
11
|
+
version: { number: 7 },
|
|
12
|
+
_links: {
|
|
13
|
+
base: 'https://confluence.example.com',
|
|
14
|
+
webui: '/pages/viewpage.action?pageId=123',
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
).toEqual({
|
|
18
|
+
id: '123',
|
|
19
|
+
type: 'page',
|
|
20
|
+
title: 'Test page',
|
|
21
|
+
spaceKey: 'DOCS',
|
|
22
|
+
version: 7,
|
|
23
|
+
url: 'https://confluence.example.com/pages/viewpage.action?pageId=123',
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { ConfluenceService, escapeSearchTextForCql } from '../confluence-service.js';
|
|
2
|
-
import { SearchService } from '../confluence-client/index.js';
|
|
2
|
+
import { ContentResourceService, SearchService } from '../confluence-client/index.js';
|
|
3
3
|
|
|
4
4
|
jest.mock('../confluence-client/index.js', () => ({
|
|
5
|
-
ContentResourceService: {
|
|
5
|
+
ContentResourceService: {
|
|
6
|
+
getContentById: jest.fn(),
|
|
7
|
+
createContent: jest.fn(),
|
|
8
|
+
update2: jest.fn(),
|
|
9
|
+
},
|
|
6
10
|
SearchService: {
|
|
7
11
|
search1: jest.fn(),
|
|
8
12
|
},
|
|
@@ -75,7 +79,7 @@ describe('ConfluenceService.searchSpaces', () => {
|
|
|
75
79
|
undefined,
|
|
76
80
|
'10',
|
|
77
81
|
'0',
|
|
78
|
-
|
|
82
|
+
'none',
|
|
79
83
|
'type=space AND title ~ "my space"'
|
|
80
84
|
);
|
|
81
85
|
});
|
|
@@ -91,7 +95,7 @@ describe('ConfluenceService.searchSpaces', () => {
|
|
|
91
95
|
undefined,
|
|
92
96
|
'5',
|
|
93
97
|
undefined,
|
|
94
|
-
|
|
98
|
+
'none',
|
|
95
99
|
'type=space AND title ~ "say \\"hello\\""'
|
|
96
100
|
);
|
|
97
101
|
});
|
|
@@ -107,7 +111,7 @@ describe('ConfluenceService.searchSpaces', () => {
|
|
|
107
111
|
undefined,
|
|
108
112
|
'5',
|
|
109
113
|
undefined,
|
|
110
|
-
|
|
114
|
+
'none',
|
|
111
115
|
'type=space AND title ~ "path\\\\to\\\\space"'
|
|
112
116
|
);
|
|
113
117
|
});
|
|
@@ -122,3 +126,155 @@ describe('ConfluenceService.searchSpaces', () => {
|
|
|
122
126
|
expect(result.error).toBeDefined();
|
|
123
127
|
});
|
|
124
128
|
});
|
|
129
|
+
|
|
130
|
+
describe('ConfluenceService token optimization paths', () => {
|
|
131
|
+
let service: ConfluenceService;
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
service = new ConfluenceService('test-host', 'test-token');
|
|
135
|
+
jest.clearAllMocks();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('keeps storage mode as the default body shape', async () => {
|
|
139
|
+
const mockContent = {
|
|
140
|
+
id: '123',
|
|
141
|
+
type: 'page',
|
|
142
|
+
title: 'Test page',
|
|
143
|
+
body: {
|
|
144
|
+
storage: {
|
|
145
|
+
value: '<p>Hello</p>',
|
|
146
|
+
representation: 'storage',
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
(ContentResourceService.getContentById as jest.Mock).mockResolvedValue(mockContent);
|
|
151
|
+
|
|
152
|
+
const result = await service.getContent('123');
|
|
153
|
+
|
|
154
|
+
expect(result.success).toBe(true);
|
|
155
|
+
expect(result.data).toBe(mockContent);
|
|
156
|
+
expect(ContentResourceService.getContentById).toHaveBeenCalledWith('123', 'body.storage');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('converts storage XML to text when bodyMode is text', async () => {
|
|
160
|
+
(ContentResourceService.getContentById as jest.Mock).mockResolvedValue({
|
|
161
|
+
id: '123',
|
|
162
|
+
type: 'page',
|
|
163
|
+
title: 'Test page',
|
|
164
|
+
body: {
|
|
165
|
+
storage: {
|
|
166
|
+
value: '<p>Hello & <strong>world</strong></p><ul><li>One</li><li>Two</li></ul>',
|
|
167
|
+
representation: 'storage',
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
version: { number: 3 },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const result = await service.getContent('123', 'version', 'text');
|
|
174
|
+
|
|
175
|
+
expect(result.success).toBe(true);
|
|
176
|
+
expect(ContentResourceService.getContentById).toHaveBeenCalledWith('123', 'version,body.storage');
|
|
177
|
+
expect(result.data).toMatchObject({
|
|
178
|
+
id: '123',
|
|
179
|
+
type: 'page',
|
|
180
|
+
title: 'Test page',
|
|
181
|
+
version: { number: 3 },
|
|
182
|
+
body: {
|
|
183
|
+
text: {
|
|
184
|
+
representation: 'text',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
expect((result.data as any).body.text.value).toContain('Hello & world');
|
|
189
|
+
expect((result.data as any).body.text.value).toContain('- One');
|
|
190
|
+
expect((result.data as any).body.text.value).toContain('- Two');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('truncates text bodies when maxBodyChars is provided', async () => {
|
|
194
|
+
(ContentResourceService.getContentById as jest.Mock).mockResolvedValue({
|
|
195
|
+
id: '123',
|
|
196
|
+
type: 'page',
|
|
197
|
+
title: 'Test page',
|
|
198
|
+
body: {
|
|
199
|
+
storage: {
|
|
200
|
+
value: '<p>Hello world</p>',
|
|
201
|
+
representation: 'storage',
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const result = await service.getContent('123', undefined, 'text', 5);
|
|
207
|
+
|
|
208
|
+
expect(result.success).toBe(true);
|
|
209
|
+
expect(result.data).toEqual({
|
|
210
|
+
id: '123',
|
|
211
|
+
type: 'page',
|
|
212
|
+
title: 'Test page',
|
|
213
|
+
body: {
|
|
214
|
+
text: {
|
|
215
|
+
value: 'Hello',
|
|
216
|
+
representation: 'text',
|
|
217
|
+
truncated: true,
|
|
218
|
+
originalLength: 11,
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('omits the body when bodyMode is none', async () => {
|
|
225
|
+
(ContentResourceService.getContentById as jest.Mock).mockResolvedValue({
|
|
226
|
+
id: '123',
|
|
227
|
+
type: 'page',
|
|
228
|
+
title: 'Test page',
|
|
229
|
+
body: {
|
|
230
|
+
storage: {
|
|
231
|
+
value: '<p>Hello</p>',
|
|
232
|
+
representation: 'storage',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
version: { number: 1 },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const result = await service.getContent('123', undefined, 'none');
|
|
239
|
+
|
|
240
|
+
expect(result.success).toBe(true);
|
|
241
|
+
expect(result.data).toEqual({
|
|
242
|
+
id: '123',
|
|
243
|
+
type: 'page',
|
|
244
|
+
title: 'Test page',
|
|
245
|
+
version: { number: 1 },
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('uses the package default limit and no excerpt for content search', async () => {
|
|
250
|
+
(SearchService.search1 as jest.Mock).mockResolvedValue({ results: [] });
|
|
251
|
+
|
|
252
|
+
await service.searchContent('type=page');
|
|
253
|
+
|
|
254
|
+
expect(SearchService.search1).toHaveBeenCalledWith(
|
|
255
|
+
undefined,
|
|
256
|
+
undefined,
|
|
257
|
+
undefined,
|
|
258
|
+
'25',
|
|
259
|
+
undefined,
|
|
260
|
+
'none',
|
|
261
|
+
'type=page'
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('forwards explicit excerpt for space search', async () => {
|
|
266
|
+
(SearchService.search1 as jest.Mock).mockResolvedValue({ results: [] });
|
|
267
|
+
|
|
268
|
+
await service.searchSpaces('docs', 5, 10, 'space.icon', 'highlight');
|
|
269
|
+
|
|
270
|
+
expect(SearchService.search1).toHaveBeenCalledWith(
|
|
271
|
+
undefined,
|
|
272
|
+
'space.icon',
|
|
273
|
+
undefined,
|
|
274
|
+
'5',
|
|
275
|
+
'10',
|
|
276
|
+
'highlight',
|
|
277
|
+
'type=space AND title ~ "docs"'
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getProductRuntimeConfig, validateProductRuntimeConfig } from '@atlassian-dc-mcp/common';
|
|
2
|
+
|
|
3
|
+
export function getConfluenceRuntimeConfig() {
|
|
4
|
+
return getProductRuntimeConfig('confluence');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getDefaultPageSize() {
|
|
8
|
+
return getConfluenceRuntimeConfig().defaultPageSize;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getMissingConfig() {
|
|
12
|
+
return validateProductRuntimeConfig('confluence');
|
|
13
|
+
}
|