@ecubelabs/atlassian-mcp 1.2.0 → 1.3.0-next.1

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.
@@ -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: `${this.config.host}/rest/api/${this.config.apiVersion}`,
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecubelabs/atlassian-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.3.0-next.1",
4
4
  "bin": "./dist/index.js",
5
5
  "repository": {
6
6
  "url": "https://github.com/Ecube-Labs/skynet.git"
@@ -37,5 +37,6 @@
37
37
  "semantic-release-yarn": "^3.0.2",
38
38
  "ts-node": "^10.9.2",
39
39
  "typescript": "^5.8.2"
40
- }
40
+ },
41
+ "stableVersion": "1.0.0"
41
42
  }