@ecubelabs/atlassian-mcp 1.4.0 → 1.5.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.
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ export const registerGetTemplateById = (server, confluenceService) => {
3
+ server.tool('get-template-by-id', 'Get a specific content template by its ID. Returns detailed template information including name, description, space, labels, and optionally the template body content.', {
4
+ contentTemplateId: z.string().describe('The ID of the content template to retrieve'),
5
+ expand: z
6
+ .array(z.enum(['body', 'body.storage', 'body.view']))
7
+ .optional()
8
+ .describe('Properties to expand in the response. Use "body" or "body.storage" to include template content in storage format.'),
9
+ }, async ({ contentTemplateId, expand }) => {
10
+ try {
11
+ const result = await confluenceService.getTemplateById(contentTemplateId, {
12
+ expand,
13
+ });
14
+ return {
15
+ content: [
16
+ {
17
+ type: 'text',
18
+ text: JSON.stringify(result),
19
+ },
20
+ ],
21
+ };
22
+ }
23
+ catch (error) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: 'text',
28
+ text: `Failed to retrieve template: ${error instanceof Error ? error.message : String(error)}`,
29
+ },
30
+ ],
31
+ };
32
+ }
33
+ });
34
+ };
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ export const registerGetTemplates = (server, confluenceService) => {
3
+ server.tool('get-templates', 'Get list of content templates from Confluence. Returns global templates if no spaceKey is provided, or space-specific templates when spaceKey is specified.', {
4
+ spaceKey: z
5
+ .string()
6
+ .optional()
7
+ .describe('The key of the space to retrieve templates from. If not specified, global templates will be returned.'),
8
+ start: z
9
+ .number()
10
+ .min(0)
11
+ .optional()
12
+ .describe('The starting index of the returned templates (for pagination). Default: 0'),
13
+ limit: z
14
+ .number()
15
+ .min(1)
16
+ .max(100)
17
+ .optional()
18
+ .describe('Maximum number of templates to return per page. Default: 25, Max: 100'),
19
+ expand: z
20
+ .array(z.enum(['body', 'body.storage', 'body.view']))
21
+ .optional()
22
+ .describe('Properties to expand in the response. Use "body" or "body.storage" to include template content.'),
23
+ }, async ({ spaceKey, start, limit, expand }) => {
24
+ try {
25
+ const result = await confluenceService.getTemplates({
26
+ spaceKey,
27
+ start,
28
+ limit,
29
+ expand,
30
+ });
31
+ return {
32
+ content: [
33
+ {
34
+ type: 'text',
35
+ text: JSON.stringify(result),
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ catch (error) {
41
+ return {
42
+ content: [
43
+ {
44
+ type: 'text',
45
+ text: `Failed to retrieve templates: ${error instanceof Error ? error.message : String(error)}`,
46
+ },
47
+ ],
48
+ };
49
+ }
50
+ });
51
+ };
@@ -12,6 +12,8 @@ export { registerGetSpaceById } from './get-space-by-id.js';
12
12
  export { registerFindSpaceByName } from './find-space-by-name.js';
13
13
  export { registerGetRecentlyUpdatedPages } from './get-recently-updated-pages.js';
14
14
  export { registerGetRecentlyCreatedPages } from './get-recently-created-pages.js';
15
+ export { registerGetTemplates } from './get-templates.js';
16
+ export { registerGetTemplateById } from './get-template-by-id.js';
15
17
  // Write tools
16
18
  export { registerCreatePage } from './create-page.js';
17
19
  export { registerUpdatePage } from './update-page.js';
@@ -1,5 +1,5 @@
1
1
  import { ConfluenceService } from './libs/confluence-client.js';
2
- import { registerGetPages, registerGetPagesForLabel, registerGetPageById, registerGetLabels, registerFindPagesByLabelName, registerFindLabelByName, registerGetPageChildren, registerGetPageChildrenTree, registerGetSpaces, registerGetSpaceById, registerFindSpaceByName, registerGetRecentlyUpdatedPages, registerGetRecentlyCreatedPages, registerCreatePage, registerUpdatePage, registerDeletePage, registerAddLabelsToPage, registerRemoveLabelFromPage, registerConfluenceStorageFormatHelp, } from './confluence-tools/index.js';
2
+ import { registerGetPages, registerGetPagesForLabel, registerGetPageById, registerGetLabels, registerFindPagesByLabelName, registerFindLabelByName, registerGetPageChildren, registerGetPageChildrenTree, registerGetSpaces, registerGetSpaceById, registerFindSpaceByName, registerGetRecentlyUpdatedPages, registerGetRecentlyCreatedPages, registerGetTemplates, registerGetTemplateById, registerCreatePage, registerUpdatePage, registerDeletePage, registerAddLabelsToPage, registerRemoveLabelFromPage, registerConfluenceStorageFormatHelp, } from './confluence-tools/index.js';
3
3
  export const registerConfluenceTools = (server) => {
4
4
  // Initialize Confluence service
5
5
  const confluenceService = new ConfluenceService();
@@ -17,6 +17,8 @@ export const registerConfluenceTools = (server) => {
17
17
  registerFindSpaceByName(server, confluenceService);
18
18
  registerGetRecentlyUpdatedPages(server, confluenceService);
19
19
  registerGetRecentlyCreatedPages(server, confluenceService);
20
+ registerGetTemplates(server, confluenceService);
21
+ registerGetTemplateById(server, confluenceService);
20
22
  // Register write tools
21
23
  registerCreatePage(server, confluenceService);
22
24
  registerUpdatePage(server, confluenceService);
@@ -8,11 +8,13 @@ export class BaseApiService {
8
8
  config;
9
9
  serviceName;
10
10
  client;
11
+ v1Client; // v1 API용 클라이언트 (Confluence 전용)
11
12
  rateLimiter;
12
13
  constructor(config, serviceName) {
13
14
  this.config = config;
14
15
  this.serviceName = serviceName;
15
16
  this.client = this.setupAxiosClient();
17
+ this.v1Client = this.setupV1AxiosClient();
16
18
  this.rateLimiter = pLimit(5); // 동시 요청 5개 제한
17
19
  this.setupInterceptors();
18
20
  this.setupRetry();
@@ -37,6 +39,24 @@ export class BaseApiService {
37
39
  },
38
40
  });
39
41
  }
42
+ /**
43
+ * Confluence v1 REST API용 클라이언트 설정
44
+ * v1 API 엔드포인트: /wiki/rest/api/...
45
+ */
46
+ setupV1AxiosClient() {
47
+ const auth = Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString('base64');
48
+ const baseURL = `${this.config.host}/wiki/rest/api`;
49
+ return axios.create({
50
+ baseURL,
51
+ headers: {
52
+ Authorization: `Basic ${auth}`,
53
+ Accept: 'application/json',
54
+ 'Content-Type': 'application/json',
55
+ },
56
+ timeout: 30000,
57
+ paramsSerializer: (params) => this.serializeConfluenceParams(params),
58
+ });
59
+ }
40
60
  serializeConfluenceParams(params) {
41
61
  const searchParams = new URLSearchParams();
42
62
  Object.entries(params).forEach(([key, value]) => {
@@ -69,9 +89,20 @@ export class BaseApiService {
69
89
  }, (error) => {
70
90
  return this.handleError(error);
71
91
  });
92
+ // v1Client에도 동일한 인터셉터 설정
93
+ this.v1Client.interceptors.request.use((config) => {
94
+ return config;
95
+ }, (error) => {
96
+ return Promise.reject(error);
97
+ });
98
+ this.v1Client.interceptors.response.use((response) => {
99
+ return response;
100
+ }, (error) => {
101
+ return this.handleError(error);
102
+ });
72
103
  }
73
104
  setupRetry() {
74
- axiosRetry(this.client, {
105
+ const retryConfig = {
75
106
  retries: 3,
76
107
  retryDelay: axiosRetry.exponentialDelay,
77
108
  retryCondition: (error) => {
@@ -79,7 +110,9 @@ export class BaseApiService {
79
110
  error.response?.status === 429 || // Rate limit
80
111
  error.response?.status === 503); // Service unavailable
81
112
  },
82
- });
113
+ };
114
+ axiosRetry(this.client, retryConfig);
115
+ axiosRetry(this.v1Client, retryConfig);
83
116
  }
84
117
  async handleError(error) {
85
118
  const { response } = error;
@@ -334,7 +334,7 @@ export class ConfluenceService extends BaseApiService {
334
334
  * 참고: 라벨 관리는 v1 API를 사용 (/wiki/rest/api/content)
335
335
  */
336
336
  async addLabelsToPage(pageId, labels) {
337
- return this.makeRequest(() => this.client.post(`/wiki/rest/api/content/${pageId}/label`, labels, {
337
+ return this.makeRequest(() => this.v1Client.post(`/content/${pageId}/label`, labels, {
338
338
  headers: {
339
339
  'Content-Type': 'application/json',
340
340
  },
@@ -349,6 +349,38 @@ export class ConfluenceService extends BaseApiService {
349
349
  const params = {
350
350
  name: labelName,
351
351
  };
352
- return this.makeRequest(() => this.client.delete(`/wiki/rest/api/content/${pageId}/label`, { params }));
352
+ return this.makeRequest(() => this.v1Client.delete(`/content/${pageId}/label`, { params }));
353
+ }
354
+ /**
355
+ * 템플릿 목록 조회
356
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-page-get
357
+ * 참고: 템플릿 API는 v1 API를 사용 (/wiki/rest/api/template)
358
+ */
359
+ async getTemplates(options) {
360
+ const params = {};
361
+ if (options) {
362
+ const { spaceKey, start, limit, expand } = options;
363
+ if (spaceKey)
364
+ params.spaceKey = spaceKey;
365
+ if (start !== undefined)
366
+ params.start = start;
367
+ if (limit !== undefined)
368
+ params.limit = limit;
369
+ if (expand)
370
+ params.expand = expand.join(',');
371
+ }
372
+ return this.makeRequest(() => this.v1Client.get('/template/page', { params }));
373
+ }
374
+ /**
375
+ * 특정 템플릿 상세 조회
376
+ * @see https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-template/#api-wiki-rest-api-template-contentTemplateId-get
377
+ * 참고: 템플릿 API는 v1 API를 사용 (/wiki/rest/api/template)
378
+ */
379
+ async getTemplateById(contentTemplateId, options) {
380
+ const params = {};
381
+ if (options?.expand) {
382
+ params.expand = options.expand.join(',');
383
+ }
384
+ return this.makeRequest(() => this.v1Client.get(`/template/page/${contentTemplateId}`, { params }));
353
385
  }
354
386
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecubelabs/atlassian-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "bin": "./dist/index.js",
5
5
  "repository": {
6
6
  "url": "https://github.com/Ecube-Labs/skynet.git"