@ecubelabs/atlassian-mcp 1.2.0 → 1.3.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/dist/config.js +6 -6
- package/dist/confluence-tools-register.js +615 -0
- package/dist/index.js +6 -1576
- package/dist/jira-tools-register.js +1593 -0
- package/dist/libs/base-client.js +29 -2
- package/dist/libs/confluence-client.js +277 -0
- package/package.json +1 -1
package/dist/libs/base-client.js
CHANGED
|
@@ -19,15 +19,42 @@ export class BaseApiService {
|
|
|
19
19
|
}
|
|
20
20
|
setupAxiosClient() {
|
|
21
21
|
const auth = Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString('base64');
|
|
22
|
+
const baseURL = this.serviceName === 'ConfluenceService'
|
|
23
|
+
? `${this.config.host}/wiki/api/v${this.config.apiVersion}`
|
|
24
|
+
: `${this.config.host}/rest/api/${this.config.apiVersion}`;
|
|
22
25
|
return axios.create({
|
|
23
|
-
baseURL
|
|
26
|
+
baseURL,
|
|
24
27
|
headers: {
|
|
25
28
|
Authorization: `Basic ${auth}`,
|
|
26
29
|
Accept: 'application/json',
|
|
27
30
|
'Content-Type': 'application/json',
|
|
28
31
|
},
|
|
29
32
|
timeout: 30000,
|
|
33
|
+
paramsSerializer: (params) => {
|
|
34
|
+
return this.serviceName === 'ConfluenceService'
|
|
35
|
+
? this.serializeConfluenceParams(params)
|
|
36
|
+
: new URLSearchParams(params).toString();
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
serializeConfluenceParams(params) {
|
|
41
|
+
const searchParams = new URLSearchParams();
|
|
42
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
43
|
+
if (value === undefined || value === null)
|
|
44
|
+
return;
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
// 배열의 경우 각 값을 개별적으로 추가 (대괄호 없이)
|
|
47
|
+
value.forEach((item) => {
|
|
48
|
+
if (item !== undefined && item !== null) {
|
|
49
|
+
searchParams.append(key, String(item));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
searchParams.append(key, String(value));
|
|
55
|
+
}
|
|
30
56
|
});
|
|
57
|
+
return searchParams.toString();
|
|
31
58
|
}
|
|
32
59
|
setupInterceptors() {
|
|
33
60
|
// Request 인터셉터
|
|
@@ -85,7 +112,7 @@ export class BaseApiService {
|
|
|
85
112
|
if (response.data?.message) {
|
|
86
113
|
return response.data.message;
|
|
87
114
|
}
|
|
88
|
-
return 'Unknown error occurred';
|
|
115
|
+
return JSON.stringify(response.data) || 'Unknown error occurred';
|
|
89
116
|
}
|
|
90
117
|
// Rate limited request wrapper
|
|
91
118
|
async makeRequest(requestFn) {
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { atlassianConfig } from '../config.js';
|
|
2
|
+
import { BaseApiService } from './base-client.js';
|
|
3
|
+
export class ConfluenceService extends BaseApiService {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(atlassianConfig.confluence, 'ConfluenceService');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 모든 페이지 목록 조회
|
|
9
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-get
|
|
10
|
+
*/
|
|
11
|
+
async getPages(options) {
|
|
12
|
+
const params = {};
|
|
13
|
+
if (options) {
|
|
14
|
+
const { id, spaceId, title, status, bodyFormat, cursor, limit, sort } = options;
|
|
15
|
+
if (id)
|
|
16
|
+
params.id = id;
|
|
17
|
+
if (spaceId)
|
|
18
|
+
params['space-id'] = spaceId;
|
|
19
|
+
if (title)
|
|
20
|
+
params.title = title;
|
|
21
|
+
if (status)
|
|
22
|
+
params.status = status;
|
|
23
|
+
if (bodyFormat)
|
|
24
|
+
params['body-format'] = bodyFormat;
|
|
25
|
+
if (cursor)
|
|
26
|
+
params.cursor = cursor;
|
|
27
|
+
if (limit)
|
|
28
|
+
params.limit = limit;
|
|
29
|
+
if (sort)
|
|
30
|
+
params.sort = sort;
|
|
31
|
+
}
|
|
32
|
+
return this.makeRequest(() => this.client.get('/pages', { params }));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 특정 페이지 조회
|
|
36
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-pages-id-get
|
|
37
|
+
*/
|
|
38
|
+
async getPageById(pageId, options) {
|
|
39
|
+
const params = {};
|
|
40
|
+
if (options) {
|
|
41
|
+
const { bodyFormat, getDraft, version } = options;
|
|
42
|
+
if (bodyFormat)
|
|
43
|
+
params['body-format'] = bodyFormat;
|
|
44
|
+
if (getDraft !== undefined)
|
|
45
|
+
params['get-draft'] = getDraft;
|
|
46
|
+
if (version !== undefined)
|
|
47
|
+
params.version = version;
|
|
48
|
+
}
|
|
49
|
+
return this.makeRequest(() => this.client.get(`/pages/${pageId}`, { params }));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 라벨로 페이지 검색
|
|
53
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/#api-wiki-api-v2-labels-id-pages-get
|
|
54
|
+
*/
|
|
55
|
+
async getPagesByLabel(labelId, options) {
|
|
56
|
+
const params = {};
|
|
57
|
+
if (options) {
|
|
58
|
+
const { bodyFormat, sort, cursor, limit } = options;
|
|
59
|
+
if (bodyFormat)
|
|
60
|
+
params['body-format'] = bodyFormat;
|
|
61
|
+
if (sort)
|
|
62
|
+
params.sort = sort;
|
|
63
|
+
if (cursor)
|
|
64
|
+
params.cursor = cursor;
|
|
65
|
+
if (limit)
|
|
66
|
+
params.limit = limit;
|
|
67
|
+
}
|
|
68
|
+
return this.makeRequest(() => this.client.get(`/labels/${labelId}/pages`, { params }));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 라벨 목록 조회
|
|
72
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-label/#api-wiki-api-v2-labels-get
|
|
73
|
+
* 참고: API는 label-id와 prefix 필터만 지원하며, 이름으로 직접 검색은 불가능
|
|
74
|
+
*/
|
|
75
|
+
async getLabels(options) {
|
|
76
|
+
const params = {};
|
|
77
|
+
if (options) {
|
|
78
|
+
const { ids, prefix, cursor, limit, sort } = options;
|
|
79
|
+
if (ids)
|
|
80
|
+
params['label-id'] = ids;
|
|
81
|
+
if (prefix)
|
|
82
|
+
params.prefix = prefix;
|
|
83
|
+
if (cursor)
|
|
84
|
+
params.cursor = cursor;
|
|
85
|
+
if (limit)
|
|
86
|
+
params.limit = limit;
|
|
87
|
+
if (sort)
|
|
88
|
+
params.sort = sort;
|
|
89
|
+
}
|
|
90
|
+
return this.makeRequest(() => this.client.get('/labels', { params }));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 레이블 이름으로 레이블 검색 (편의 함수)
|
|
94
|
+
* 참고: API가 이름 검색을 직접 지원하지 않으므로 모든 레이블을 조회하여 클라이언트 측에서 필터링
|
|
95
|
+
* @param labelName 검색할 레이블 이름 (정확한 일치)
|
|
96
|
+
*/
|
|
97
|
+
async findLabelByName(labelName) {
|
|
98
|
+
try {
|
|
99
|
+
let cursor;
|
|
100
|
+
// 모든 레이블을 페이지별로 가져와서 검색
|
|
101
|
+
do {
|
|
102
|
+
const result = await this.getLabels({ cursor, limit: 250 });
|
|
103
|
+
// 현재 페이지에서 일치하는 레이블을 먼저 확인
|
|
104
|
+
const found = result.results.find((label) => label.name === labelName);
|
|
105
|
+
if (found) {
|
|
106
|
+
return found;
|
|
107
|
+
}
|
|
108
|
+
// 다음 페이지가 있는지 확인 (_links.next 존재 여부로 판단)
|
|
109
|
+
cursor = result._links?.next ? result.cursor : undefined;
|
|
110
|
+
} while (cursor);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
console.error(`Error finding label by name "${labelName}":`, error);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 레이블 이름으로 검색하여 페이지 조회 (편의 함수)
|
|
120
|
+
* 참고: API 제한으로 인해 모든 레이블을 조회 후 클라이언트 측에서 필터링합니다
|
|
121
|
+
* @param labelName 검색할 레이블 이름 (정확한 일치)
|
|
122
|
+
* @param options 페이지 검색 옵션
|
|
123
|
+
*/
|
|
124
|
+
async getPagesByLabelName(labelName, options) {
|
|
125
|
+
// 먼저 레이블 찾기
|
|
126
|
+
const label = await this.findLabelByName(labelName);
|
|
127
|
+
if (!label) {
|
|
128
|
+
// 레이블을 찾지 못한 경우 빈 결과를 반환하고 디버그 정보 제공
|
|
129
|
+
return {
|
|
130
|
+
results: [],
|
|
131
|
+
labelInfo: undefined,
|
|
132
|
+
_links: {
|
|
133
|
+
base: `${this.client.defaults.baseURL}`,
|
|
134
|
+
},
|
|
135
|
+
error: `Label "${labelName}" not found. Please check if the label exists and has the correct name.`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// 레이블 ID로 페이지 검색
|
|
139
|
+
const result = await this.getPagesByLabel(label.id, options);
|
|
140
|
+
return {
|
|
141
|
+
...result,
|
|
142
|
+
labelInfo: label,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 페이지의 하위 페이지 목록 조회
|
|
147
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-children/#api-wiki-api-v2-pages-id-children-get
|
|
148
|
+
* @param pageId 부모 페이지 ID
|
|
149
|
+
* @param options 조회 옵션
|
|
150
|
+
*/
|
|
151
|
+
async getPageChildren(pageId, options) {
|
|
152
|
+
const params = {};
|
|
153
|
+
if (options) {
|
|
154
|
+
const { cursor, limit, sort } = options;
|
|
155
|
+
if (cursor)
|
|
156
|
+
params.cursor = cursor;
|
|
157
|
+
if (limit)
|
|
158
|
+
params.limit = limit;
|
|
159
|
+
if (sort)
|
|
160
|
+
params.sort = sort;
|
|
161
|
+
}
|
|
162
|
+
return this.makeRequest(() => this.client.get(`/pages/${pageId}/children`, { params }));
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 페이지의 전체 하위 트리를 재귀적으로 가져오는 편의 함수
|
|
166
|
+
* @param pageId 루트 페이지 ID
|
|
167
|
+
* @param options 조회 옵션
|
|
168
|
+
* @param maxDepth 최대 깊이 (기본값: 10, 무제한: -1)
|
|
169
|
+
* @param currentDepth 현재 깊이 (내부 사용)
|
|
170
|
+
*/
|
|
171
|
+
async getPageChildrenTree(pageId, options, maxDepth = 10, currentDepth = 0) {
|
|
172
|
+
// 최대 깊이 제한 확인
|
|
173
|
+
if (maxDepth !== -1 && currentDepth >= maxDepth) {
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
// 현재 페이지의 직접 자식들 가져오기
|
|
178
|
+
const result = await this.getPageChildren(pageId, options);
|
|
179
|
+
const childrenWithSubTree = [];
|
|
180
|
+
// 각 자식에 대해 재귀적으로 하위 트리 가져오기
|
|
181
|
+
for (const child of result.results) {
|
|
182
|
+
const childWithTree = { ...child };
|
|
183
|
+
// 재귀적으로 하위 트리 가져오기
|
|
184
|
+
const subTree = await this.getPageChildrenTree(child.id, options, maxDepth, currentDepth + 1);
|
|
185
|
+
if (subTree.length > 0) {
|
|
186
|
+
childWithTree.children = subTree;
|
|
187
|
+
}
|
|
188
|
+
childrenWithSubTree.push(childWithTree);
|
|
189
|
+
}
|
|
190
|
+
return childrenWithSubTree;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.error(`Error fetching children for page ${pageId} at depth ${currentDepth}:`, error);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 모든 Space 목록 조회
|
|
199
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space/#api-wiki-api-v2-spaces-get
|
|
200
|
+
*/
|
|
201
|
+
async getSpaces(options) {
|
|
202
|
+
const params = {};
|
|
203
|
+
if (options) {
|
|
204
|
+
const { id, keys, type, status, sort, cursor, limit } = options;
|
|
205
|
+
if (id)
|
|
206
|
+
params.id = id;
|
|
207
|
+
if (keys)
|
|
208
|
+
params.key = keys;
|
|
209
|
+
if (type)
|
|
210
|
+
params.type = type;
|
|
211
|
+
if (status)
|
|
212
|
+
params.status = status;
|
|
213
|
+
if (sort)
|
|
214
|
+
params.sort = sort;
|
|
215
|
+
if (cursor)
|
|
216
|
+
params.cursor = cursor;
|
|
217
|
+
if (limit)
|
|
218
|
+
params.limit = limit;
|
|
219
|
+
}
|
|
220
|
+
return this.makeRequest(() => this.client.get('/spaces', { params }));
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* 특정 Space 조회
|
|
224
|
+
* @see https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-space/#api-wiki-api-v2-spaces-id-get
|
|
225
|
+
*/
|
|
226
|
+
async getSpaceById(spaceId) {
|
|
227
|
+
return this.makeRequest(() => this.client.get(`/spaces/${spaceId}`));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Space name으로 Space 검색 (편의 함수)
|
|
231
|
+
* 참고: API가 이름 검색을 직접 지원하지 않으므로 모든 Space를 조회하여 클라이언트 측에서 필터링
|
|
232
|
+
* @param spaceName 검색할 Space 이름 (정확한 일치)
|
|
233
|
+
*/
|
|
234
|
+
async findSpaceByName(spaceName) {
|
|
235
|
+
try {
|
|
236
|
+
let cursor;
|
|
237
|
+
// 모든 Space를 페이지별로 가져와서 검색
|
|
238
|
+
do {
|
|
239
|
+
const result = await this.getSpaces({ cursor, limit: 250 });
|
|
240
|
+
// 현재 페이지에서 일치하는 Space를 먼저 확인
|
|
241
|
+
const found = result.results.find((space) => space.name === spaceName);
|
|
242
|
+
if (found) {
|
|
243
|
+
return found;
|
|
244
|
+
}
|
|
245
|
+
// 다음 페이지가 있는지 확인 (_links.next 존재 여부로 판단)
|
|
246
|
+
cursor = result._links?.next ? result.cursor : undefined;
|
|
247
|
+
} while (cursor);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
console.error(`Error finding space by name "${spaceName}":`, error);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 최신 문서 조회 (최근 수정된 페이지들)
|
|
257
|
+
* @param options 조회 옵션
|
|
258
|
+
*/
|
|
259
|
+
async getRecentlyUpdatedPages(options) {
|
|
260
|
+
return this.getPages({
|
|
261
|
+
...options,
|
|
262
|
+
sort: '-modified-date', // 최근 수정일 기준 내림차순
|
|
263
|
+
status: ['current'], // 현재 상태인 페이지만
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 최신 생성된 문서 조회
|
|
268
|
+
* @param options 조회 옵션
|
|
269
|
+
*/
|
|
270
|
+
async getRecentlyCreatedPages(options) {
|
|
271
|
+
return this.getPages({
|
|
272
|
+
...options,
|
|
273
|
+
sort: '-created-date', // 최근 생성일 기준 내림차순
|
|
274
|
+
status: ['current'], // 현재 상태인 페이지만
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|