@ecubelabs/atlassian-mcp 1.0.0-next.3
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 +22 -0
- package/dist/index.js +313 -0
- package/dist/libs/base-client.js +117 -0
- package/dist/libs/error-handler.js +21 -0
- package/dist/libs/jira-client.js +123 -0
- package/package.json +37 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const atlassianConfig = {
|
|
2
|
+
jira: {
|
|
3
|
+
host: process.env.JIRA_HOST || "",
|
|
4
|
+
email: process.env.JIRA_EMAIL || "",
|
|
5
|
+
apiToken: process.env.JIRA_API_TOKEN || "",
|
|
6
|
+
apiVersion: process.env.JIRA_API_VERSION || "3",
|
|
7
|
+
},
|
|
8
|
+
// confluence: {
|
|
9
|
+
// host: process.env.CONFLUENCE_HOST || "https://your-domain.atlassian.net",
|
|
10
|
+
// email: process.env.CONFLUENCE_EMAIL || "",
|
|
11
|
+
// apiToken: process.env.CONFLUENCE_API_TOKEN || "",
|
|
12
|
+
// apiVersion: process.env.CONFLUENCE_API_VERSION || "2",
|
|
13
|
+
// },
|
|
14
|
+
retry: {
|
|
15
|
+
maxRetries: 3,
|
|
16
|
+
retryDelay: 1000,
|
|
17
|
+
},
|
|
18
|
+
rateLimit: {
|
|
19
|
+
maxConcurrent: 5,
|
|
20
|
+
minTime: 100,
|
|
21
|
+
},
|
|
22
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { JiraService } from "./libs/jira-client.js";
|
|
5
|
+
// Create server instance
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "jira",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
});
|
|
10
|
+
// Initialize Jira service
|
|
11
|
+
const jiraService = new JiraService();
|
|
12
|
+
// Register Jira tools
|
|
13
|
+
server.tool("get-issue", "Get details of a Jira issue by key", {
|
|
14
|
+
issueKey: z.string().describe("Jira issue key (e.g. PROJ-123)"),
|
|
15
|
+
expand: z
|
|
16
|
+
.enum([
|
|
17
|
+
"renderedFields",
|
|
18
|
+
"names",
|
|
19
|
+
"schema",
|
|
20
|
+
"transitions",
|
|
21
|
+
"editmeta",
|
|
22
|
+
"changelog",
|
|
23
|
+
"versionedRepresentations",
|
|
24
|
+
])
|
|
25
|
+
.array()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe("Optional fields to expand in the response. Options include: renderedFields (HTML format), names (display names), schema (field type descriptions), transitions (possible transitions), editmeta (field editing info), changelog (recent updates), versionedRepresentations (field value history)"),
|
|
28
|
+
}, async ({ issueKey, expand }) => {
|
|
29
|
+
try {
|
|
30
|
+
const issue = await jiraService.getIssue(issueKey, expand);
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: JSON.stringify(issue),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `Failed to retrieve issue: ${error instanceof Error ? error.message : String(error)}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
server.tool("search-issues", "Search Jira issues using JQL", {
|
|
52
|
+
jql: z
|
|
53
|
+
.string()
|
|
54
|
+
.describe("JQL query string. Must be a bounded query with search restrictions."),
|
|
55
|
+
fields: z
|
|
56
|
+
.array(z.string())
|
|
57
|
+
.optional()
|
|
58
|
+
.describe("Optional fields to include in the response. Examples: ['summary', 'comment'] or ['*all', '-comment']"),
|
|
59
|
+
maxResults: z
|
|
60
|
+
.number()
|
|
61
|
+
.min(1)
|
|
62
|
+
.max(5000)
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("Maximum number of results to return per page (default: 50, max: 5000)"),
|
|
65
|
+
nextPageToken: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Token for fetching the next page of results. The first page has null nextPageToken."),
|
|
69
|
+
expand: z
|
|
70
|
+
.enum([
|
|
71
|
+
"renderedFields",
|
|
72
|
+
"names",
|
|
73
|
+
"schema",
|
|
74
|
+
"transitions",
|
|
75
|
+
"editmeta",
|
|
76
|
+
"changelog",
|
|
77
|
+
"versionedRepresentations",
|
|
78
|
+
])
|
|
79
|
+
.array()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Comma-separated values to expand in response: renderedFields, names, schema, transitions, operations, editmeta, changelog, versionedRepresentations"),
|
|
82
|
+
properties: z
|
|
83
|
+
.array(z.string())
|
|
84
|
+
.max(5)
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("List of up to 5 issue properties to include in results"),
|
|
87
|
+
fieldsByKeys: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Reference fields by their key rather than ID (default: false)"),
|
|
91
|
+
failFast: z
|
|
92
|
+
.boolean()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Fail request early if we can't retrieve all field data (default: false)"),
|
|
95
|
+
reconcileIssues: z
|
|
96
|
+
.array(z.number())
|
|
97
|
+
.max(50)
|
|
98
|
+
.optional()
|
|
99
|
+
.describe("Strong consistency issue IDs to be reconciled with search results (max: 50 IDs)"),
|
|
100
|
+
}, async ({ jql, fields, maxResults, nextPageToken, expand, properties, fieldsByKeys, failFast, reconcileIssues, }) => {
|
|
101
|
+
try {
|
|
102
|
+
const issues = await jiraService.searchIssues({
|
|
103
|
+
jql,
|
|
104
|
+
fields,
|
|
105
|
+
maxResults,
|
|
106
|
+
nextPageToken,
|
|
107
|
+
expand,
|
|
108
|
+
properties,
|
|
109
|
+
fieldsByKeys,
|
|
110
|
+
failFast,
|
|
111
|
+
reconcileIssues,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: JSON.stringify(issues),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
content: [
|
|
125
|
+
{
|
|
126
|
+
type: "text",
|
|
127
|
+
text: `Failed to search issues: ${error instanceof Error ? error.message : String(error)}`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
server.tool("get-comments", "Get comments for a Jira issue", {
|
|
134
|
+
issueKey: z.string().describe("Jira issue key (e.g. PROJ-123)"),
|
|
135
|
+
startAt: z
|
|
136
|
+
.number()
|
|
137
|
+
.min(0)
|
|
138
|
+
.optional()
|
|
139
|
+
.describe("The index of the first item to return (page offset). Default: 0"),
|
|
140
|
+
maxResults: z
|
|
141
|
+
.number()
|
|
142
|
+
.min(1)
|
|
143
|
+
.max(100)
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("Maximum number of items per page. Default: 100"),
|
|
146
|
+
orderBy: z
|
|
147
|
+
.enum(["created", "-created", "+created"])
|
|
148
|
+
.optional()
|
|
149
|
+
.describe("Order comments by creation date. Default: created"),
|
|
150
|
+
expand: z
|
|
151
|
+
.array(z.enum(["renderedBody"]))
|
|
152
|
+
.optional()
|
|
153
|
+
.describe("Include additional information: renderedBody (comment body in HTML format)"),
|
|
154
|
+
}, async ({ issueKey, startAt, maxResults, orderBy, expand }) => {
|
|
155
|
+
try {
|
|
156
|
+
const result = await jiraService.getComments(issueKey, {
|
|
157
|
+
startAt,
|
|
158
|
+
maxResults,
|
|
159
|
+
orderBy,
|
|
160
|
+
expand,
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: "text",
|
|
166
|
+
text: JSON.stringify(result),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
content: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: `Failed to retrieve comments: ${error instanceof Error ? error.message : String(error)}`,
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
server.tool("get-projects", "Get list of Jira projects", {
|
|
183
|
+
startAt: z
|
|
184
|
+
.number()
|
|
185
|
+
.min(0)
|
|
186
|
+
.optional()
|
|
187
|
+
.describe("The index of the first item to return (page offset). Default: 0"),
|
|
188
|
+
maxResults: z
|
|
189
|
+
.number()
|
|
190
|
+
.min(1)
|
|
191
|
+
.max(100)
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Maximum number of items per page. Default: 50, Maximum: 100"),
|
|
194
|
+
orderBy: z
|
|
195
|
+
.enum([
|
|
196
|
+
"category",
|
|
197
|
+
"-category",
|
|
198
|
+
"+category",
|
|
199
|
+
"key",
|
|
200
|
+
"-key",
|
|
201
|
+
"+key",
|
|
202
|
+
"name",
|
|
203
|
+
"-name",
|
|
204
|
+
"+name",
|
|
205
|
+
"owner",
|
|
206
|
+
"-owner",
|
|
207
|
+
"+owner",
|
|
208
|
+
"issueCount",
|
|
209
|
+
"-issueCount",
|
|
210
|
+
"+issueCount",
|
|
211
|
+
"lastIssueUpdatedTime",
|
|
212
|
+
"-lastIssueUpdatedTime",
|
|
213
|
+
"+lastIssueUpdatedTime",
|
|
214
|
+
"archivedDate",
|
|
215
|
+
"-archivedDate",
|
|
216
|
+
"+archivedDate",
|
|
217
|
+
"deletedDate",
|
|
218
|
+
"-deletedDate",
|
|
219
|
+
"+deletedDate",
|
|
220
|
+
])
|
|
221
|
+
.optional()
|
|
222
|
+
.describe("Field to order results by. Default: key"),
|
|
223
|
+
id: z
|
|
224
|
+
.array(z.number().int())
|
|
225
|
+
.max(50)
|
|
226
|
+
.optional()
|
|
227
|
+
.describe("Filter by project IDs. Maximum: 50 IDs"),
|
|
228
|
+
keys: z
|
|
229
|
+
.array(z.string())
|
|
230
|
+
.max(50)
|
|
231
|
+
.optional()
|
|
232
|
+
.describe("Filter by project keys. Maximum: 50 keys"),
|
|
233
|
+
query: z
|
|
234
|
+
.string()
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("Filter by matching key or name (case insensitive)"),
|
|
237
|
+
typeKey: z
|
|
238
|
+
.string()
|
|
239
|
+
.optional()
|
|
240
|
+
.describe("Filter by project type. Accepts comma-separated list: business, service_desk, software"),
|
|
241
|
+
categoryId: z
|
|
242
|
+
.number()
|
|
243
|
+
.int()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("Filter by project category ID"),
|
|
246
|
+
action: z
|
|
247
|
+
.enum(["view", "browse", "edit", "create"])
|
|
248
|
+
.optional()
|
|
249
|
+
.describe("Filter by user's project permissions. Default: view"),
|
|
250
|
+
expand: z
|
|
251
|
+
.array(z.enum([
|
|
252
|
+
"description",
|
|
253
|
+
"projectKeys",
|
|
254
|
+
"lead",
|
|
255
|
+
"issueTypes",
|
|
256
|
+
"url",
|
|
257
|
+
"insight",
|
|
258
|
+
]))
|
|
259
|
+
.optional()
|
|
260
|
+
.describe("Additional fields to include: description, projectKeys, lead, issueTypes, url, insight"),
|
|
261
|
+
status: z
|
|
262
|
+
.array(z.enum(["live", "archived", "deleted"]))
|
|
263
|
+
.optional()
|
|
264
|
+
.describe("EXPERIMENTAL. Filter by project status"),
|
|
265
|
+
}, async ({ startAt, maxResults, orderBy, id, keys, query, typeKey, categoryId, action, expand, status, }) => {
|
|
266
|
+
try {
|
|
267
|
+
const projects = await jiraService.getProjects({
|
|
268
|
+
startAt,
|
|
269
|
+
maxResults,
|
|
270
|
+
orderBy,
|
|
271
|
+
id,
|
|
272
|
+
keys,
|
|
273
|
+
query,
|
|
274
|
+
typeKey,
|
|
275
|
+
categoryId,
|
|
276
|
+
action,
|
|
277
|
+
expand,
|
|
278
|
+
status,
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
content: [
|
|
282
|
+
{
|
|
283
|
+
type: "text",
|
|
284
|
+
text: JSON.stringify(projects),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
return {
|
|
291
|
+
content: [
|
|
292
|
+
{
|
|
293
|
+
type: "text",
|
|
294
|
+
text: `Failed to retrieve projects: ${error instanceof Error ? error.message : String(error)}`,
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
// Start the server
|
|
301
|
+
async function main() {
|
|
302
|
+
const transport = new StdioServerTransport();
|
|
303
|
+
await server.connect(transport);
|
|
304
|
+
/**
|
|
305
|
+
* stdout 을 사용하면 stdio server 특성상 문제가 되므로 필요한 출력이 있다면 stderr를 사용한다.
|
|
306
|
+
* @see https://modelcontextprotocol.io/quickstart/server#node
|
|
307
|
+
*/
|
|
308
|
+
console.error("Jira MCP Server running on stdio");
|
|
309
|
+
}
|
|
310
|
+
main().catch((error) => {
|
|
311
|
+
console.error("Fatal error in main():", error);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import axiosRetry from "axios-retry";
|
|
3
|
+
import pLimit from "p-limit";
|
|
4
|
+
/**
|
|
5
|
+
* 기본적인 API Token 관리, 인터셉터, rate limit/retry 관리, 에러메세지 핸들링 등 Jira/Confluence 공통으로 필요한 기능을 제공하는 베이스 클래스
|
|
6
|
+
*/
|
|
7
|
+
export class BaseApiService {
|
|
8
|
+
config;
|
|
9
|
+
serviceName;
|
|
10
|
+
client;
|
|
11
|
+
rateLimiter;
|
|
12
|
+
constructor(config, serviceName) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.serviceName = serviceName;
|
|
15
|
+
this.client = this.setupAxiosClient();
|
|
16
|
+
this.rateLimiter = pLimit(5); // 동시 요청 5개 제한
|
|
17
|
+
this.setupInterceptors();
|
|
18
|
+
this.setupRetry();
|
|
19
|
+
}
|
|
20
|
+
setupAxiosClient() {
|
|
21
|
+
const auth = Buffer.from(`${this.config.email}:${this.config.apiToken}`).toString("base64");
|
|
22
|
+
return axios.create({
|
|
23
|
+
baseURL: `${this.config.host}/rest/api/${this.config.apiVersion}`,
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Basic ${auth}`,
|
|
26
|
+
Accept: "application/json",
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
},
|
|
29
|
+
timeout: 30000,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
setupInterceptors() {
|
|
33
|
+
// Request 인터셉터
|
|
34
|
+
this.client.interceptors.request.use((config) => {
|
|
35
|
+
return config;
|
|
36
|
+
}, (error) => {
|
|
37
|
+
return Promise.reject(error);
|
|
38
|
+
});
|
|
39
|
+
// Response 인터셉터
|
|
40
|
+
this.client.interceptors.response.use((response) => {
|
|
41
|
+
return response;
|
|
42
|
+
}, (error) => {
|
|
43
|
+
return this.handleError(error);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
setupRetry() {
|
|
47
|
+
axiosRetry(this.client, {
|
|
48
|
+
retries: 3,
|
|
49
|
+
retryDelay: axiosRetry.exponentialDelay,
|
|
50
|
+
retryCondition: (error) => {
|
|
51
|
+
return (axiosRetry.isNetworkOrIdempotentRequestError(error) ||
|
|
52
|
+
error.response?.status === 429 || // Rate limit
|
|
53
|
+
error.response?.status === 503); // Service unavailable
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async handleError(error) {
|
|
58
|
+
const { response } = error;
|
|
59
|
+
if (response) {
|
|
60
|
+
const errorMessage = this.extractErrorMessage(response);
|
|
61
|
+
switch (response.status) {
|
|
62
|
+
case 401:
|
|
63
|
+
throw new Error("Authentication failed");
|
|
64
|
+
case 403:
|
|
65
|
+
throw new Error("Permission denied");
|
|
66
|
+
case 404:
|
|
67
|
+
throw new Error("Resource not found");
|
|
68
|
+
case 429:
|
|
69
|
+
throw error;
|
|
70
|
+
default:
|
|
71
|
+
throw new Error(errorMessage);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
throw new Error("Network error occurred");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
extractErrorMessage(response) {
|
|
79
|
+
if (response.data?.errorMessages) {
|
|
80
|
+
return response.data.errorMessages.join(", ");
|
|
81
|
+
}
|
|
82
|
+
if (response.data?.errors) {
|
|
83
|
+
return Object.values(response.data.errors).join(", ");
|
|
84
|
+
}
|
|
85
|
+
if (response.data?.message) {
|
|
86
|
+
return response.data.message;
|
|
87
|
+
}
|
|
88
|
+
return "Unknown error occurred";
|
|
89
|
+
}
|
|
90
|
+
// Rate limited request wrapper
|
|
91
|
+
async makeRequest(requestFn) {
|
|
92
|
+
return this.rateLimiter(async () => {
|
|
93
|
+
const response = await requestFn();
|
|
94
|
+
return response.data;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Pagination helper
|
|
98
|
+
async *paginate(endpoint, params = {}, pageSize = 50) {
|
|
99
|
+
let startAt = 0;
|
|
100
|
+
let isLast = false;
|
|
101
|
+
while (!isLast) {
|
|
102
|
+
const response = await this.makeRequest(() => this.client.get(endpoint, {
|
|
103
|
+
params: {
|
|
104
|
+
...params,
|
|
105
|
+
startAt,
|
|
106
|
+
maxResults: pageSize,
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
yield response.values || response.results || [];
|
|
110
|
+
isLast = response.isLast !== false;
|
|
111
|
+
startAt += pageSize;
|
|
112
|
+
if (!response.values && !response.results) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
statusCode;
|
|
3
|
+
details;
|
|
4
|
+
constructor(message, statusCode, details) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.statusCode = statusCode;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = "ApiError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function handleApiError(error) {
|
|
12
|
+
if (error.response) {
|
|
13
|
+
throw new ApiError(error.response.data?.message || "API request failed", error.response.status, error.response.data);
|
|
14
|
+
}
|
|
15
|
+
else if (error.request) {
|
|
16
|
+
throw new ApiError("No response received from server");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
throw new ApiError(error.message || "Unknown error occurred");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { BaseApiService } from "./base-client.js";
|
|
2
|
+
import { atlassianConfig } from "../config.js";
|
|
3
|
+
export class JiraService extends BaseApiService {
|
|
4
|
+
constructor() {
|
|
5
|
+
super(atlassianConfig.jira, "JiraService");
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Issue 조회
|
|
9
|
+
* @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-issueidorkey-get
|
|
10
|
+
*/
|
|
11
|
+
async getIssue(issueKey, expand) {
|
|
12
|
+
return this.makeRequest(() => this.client.get(`/issue/${issueKey}`, {
|
|
13
|
+
params: expand ? { expand: expand.join(",") } : undefined,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* JQL 검색
|
|
18
|
+
* @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-get
|
|
19
|
+
*/
|
|
20
|
+
async searchIssues(options) {
|
|
21
|
+
const allIssues = [];
|
|
22
|
+
const { jql, nextPageToken, maxResults = 50, fields, expand, properties, fieldsByKeys, failFast, reconcileIssues, } = options;
|
|
23
|
+
// Prepare parameters for API request
|
|
24
|
+
const params = {
|
|
25
|
+
jql,
|
|
26
|
+
maxResults,
|
|
27
|
+
};
|
|
28
|
+
// Add optional parameters if provided
|
|
29
|
+
if (nextPageToken)
|
|
30
|
+
params.nextPageToken = nextPageToken;
|
|
31
|
+
if (fields)
|
|
32
|
+
params.fields = fields.join(",");
|
|
33
|
+
if (expand)
|
|
34
|
+
params.expand = expand.join(",");
|
|
35
|
+
if (properties)
|
|
36
|
+
params.properties = properties.join(",");
|
|
37
|
+
if (fieldsByKeys !== undefined)
|
|
38
|
+
params.fieldsByKeys = fieldsByKeys;
|
|
39
|
+
if (failFast !== undefined)
|
|
40
|
+
params.failFast = failFast;
|
|
41
|
+
if (reconcileIssues)
|
|
42
|
+
params.reconcileIssues = reconcileIssues.join(",");
|
|
43
|
+
// Single page retrieval when nextPageToken is provided
|
|
44
|
+
if (nextPageToken) {
|
|
45
|
+
const response = await this.makeRequest(() => this.client.get("/search/jql", { params }));
|
|
46
|
+
return response.issues || [];
|
|
47
|
+
}
|
|
48
|
+
// Use pagination for complete result set
|
|
49
|
+
let token = null;
|
|
50
|
+
do {
|
|
51
|
+
if (token)
|
|
52
|
+
params.nextPageToken = token;
|
|
53
|
+
const response = await this.makeRequest(() => this.client.get("/search/jql", { params }));
|
|
54
|
+
if (response.issues) {
|
|
55
|
+
allIssues.push(...response.issues);
|
|
56
|
+
}
|
|
57
|
+
token = response.nextPageToken || null;
|
|
58
|
+
} while (token);
|
|
59
|
+
return allIssues;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 프로젝트 목록 조회
|
|
63
|
+
* @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-projects/#api-rest-api-3-project-search-get
|
|
64
|
+
*/
|
|
65
|
+
async getProjects(options) {
|
|
66
|
+
const params = {};
|
|
67
|
+
// Add optional parameters if provided
|
|
68
|
+
if (options) {
|
|
69
|
+
const { startAt, maxResults, orderBy, id, keys, query, typeKey, categoryId, action, expand, status, properties, propertyQuery } = options;
|
|
70
|
+
if (startAt !== undefined)
|
|
71
|
+
params.startAt = startAt;
|
|
72
|
+
if (maxResults !== undefined)
|
|
73
|
+
params.maxResults = maxResults;
|
|
74
|
+
if (orderBy)
|
|
75
|
+
params.orderBy = orderBy;
|
|
76
|
+
if (id)
|
|
77
|
+
params.id = id.join(',');
|
|
78
|
+
if (keys)
|
|
79
|
+
params.keys = keys.join(',');
|
|
80
|
+
if (query)
|
|
81
|
+
params.query = query;
|
|
82
|
+
if (typeKey)
|
|
83
|
+
params.typeKey = typeKey;
|
|
84
|
+
if (categoryId !== undefined)
|
|
85
|
+
params.categoryId = categoryId;
|
|
86
|
+
if (action)
|
|
87
|
+
params.action = action;
|
|
88
|
+
if (expand)
|
|
89
|
+
params.expand = expand.join(',');
|
|
90
|
+
if (status)
|
|
91
|
+
params.status = status.join(',');
|
|
92
|
+
if (properties) {
|
|
93
|
+
// Handle the nested StringList structure
|
|
94
|
+
properties.forEach((prop, index) => {
|
|
95
|
+
params[`properties[${index}]`] = prop.join(',');
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (propertyQuery)
|
|
99
|
+
params.propertyQuery = propertyQuery;
|
|
100
|
+
}
|
|
101
|
+
return this.makeRequest(() => this.client.get("/project/search", { params }));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* 이슈 댓글 목록 조회
|
|
105
|
+
* @see https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-comments/#api-rest-api-3-issue-issueidorkey-comment-get
|
|
106
|
+
*/
|
|
107
|
+
async getComments(issueKey, options) {
|
|
108
|
+
const params = {};
|
|
109
|
+
// Add optional parameters if provided
|
|
110
|
+
if (options) {
|
|
111
|
+
const { startAt, maxResults, orderBy, expand } = options;
|
|
112
|
+
if (startAt !== undefined)
|
|
113
|
+
params.startAt = startAt;
|
|
114
|
+
if (maxResults !== undefined)
|
|
115
|
+
params.maxResults = maxResults;
|
|
116
|
+
if (orderBy)
|
|
117
|
+
params.orderBy = orderBy;
|
|
118
|
+
if (expand)
|
|
119
|
+
params.expand = expand.join(',');
|
|
120
|
+
}
|
|
121
|
+
return this.makeRequest(() => this.client.get(`/issue/${issueKey}/comment`, { params }));
|
|
122
|
+
}
|
|
123
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ecubelabs/atlassian-mcp",
|
|
3
|
+
"version": "1.0.0-next.3",
|
|
4
|
+
"bin": "./dist/index.js",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/Ecube-Labs/skynet.git"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc && node -e \"require('fs').chmodSync('dist/index.js', '755')\"",
|
|
10
|
+
"prepack": "yarn build"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">= 18"
|
|
17
|
+
},
|
|
18
|
+
"packageManager": "yarn@4.1.0",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.17.3",
|
|
22
|
+
"axios": "^1.7.9",
|
|
23
|
+
"axios-retry": "^4.5.0",
|
|
24
|
+
"dotenv": "^17.2.0",
|
|
25
|
+
"p-limit": "^5.0.0",
|
|
26
|
+
"winston": "^3.17.0",
|
|
27
|
+
"zod": "^3.24.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.14.8",
|
|
31
|
+
"semantic-release": "^24.2.7",
|
|
32
|
+
"semantic-release-yarn": "^3.0.2",
|
|
33
|
+
"ts-node": "^10.9.2",
|
|
34
|
+
"typescript": "^5.8.2"
|
|
35
|
+
},
|
|
36
|
+
"stableVersion": "1.0.0"
|
|
37
|
+
}
|