@universal-mcp-toolkit/server-jira 0.1.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/.well-known/mcp-server.json +87 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +240 -0
- package/dist/index.js +1124 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Buffer } from "buffer";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import {
|
|
5
|
+
ExternalServiceError,
|
|
6
|
+
ToolkitServer,
|
|
7
|
+
ValidationError,
|
|
8
|
+
createServerCard,
|
|
9
|
+
defineTool,
|
|
10
|
+
loadEnv,
|
|
11
|
+
parseRuntimeOptions,
|
|
12
|
+
runToolkitServer
|
|
13
|
+
} from "@universal-mcp-toolkit/core";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var PACKAGE_NAME = "@universal-mcp-toolkit/server-jira";
|
|
16
|
+
var SERVER_VERSION = "0.1.0";
|
|
17
|
+
var REQUIRED_ENV_VAR_NAMES = ["JIRA_BASE_URL", "JIRA_EMAIL", "JIRA_API_TOKEN"];
|
|
18
|
+
var OPTIONAL_ENV_VAR_NAMES = ["JIRA_DEFAULT_PROJECT_KEY"];
|
|
19
|
+
var TOOL_NAMES = ["get_issue", "search_issues", "transition_issue"];
|
|
20
|
+
var RESOURCE_NAMES = ["project"];
|
|
21
|
+
var PROMPT_NAMES = ["incident_triage"];
|
|
22
|
+
var DEFAULT_SEARCH_FIELDS = [
|
|
23
|
+
"summary",
|
|
24
|
+
"status",
|
|
25
|
+
"assignee",
|
|
26
|
+
"reporter",
|
|
27
|
+
"priority",
|
|
28
|
+
"issuetype",
|
|
29
|
+
"project",
|
|
30
|
+
"created",
|
|
31
|
+
"updated"
|
|
32
|
+
];
|
|
33
|
+
var DEFAULT_ISSUE_FIELDS = [...DEFAULT_SEARCH_FIELDS, "description", "labels", "comment"];
|
|
34
|
+
var jiraEnvironmentShape = {
|
|
35
|
+
JIRA_BASE_URL: z.string().url().transform((value) => value.replace(/\/+$/, "")),
|
|
36
|
+
JIRA_EMAIL: z.string().email(),
|
|
37
|
+
JIRA_API_TOKEN: z.string().trim().min(1),
|
|
38
|
+
JIRA_DEFAULT_PROJECT_KEY: z.string().trim().min(1).optional()
|
|
39
|
+
};
|
|
40
|
+
var jiraUserSchema = z.object({
|
|
41
|
+
accountId: z.string().optional(),
|
|
42
|
+
displayName: z.string(),
|
|
43
|
+
emailAddress: z.string().nullable().optional()
|
|
44
|
+
});
|
|
45
|
+
var jiraStatusSchema = z.object({
|
|
46
|
+
id: z.string().optional(),
|
|
47
|
+
name: z.string(),
|
|
48
|
+
category: z.string().optional()
|
|
49
|
+
});
|
|
50
|
+
var jiraPrioritySchema = z.object({
|
|
51
|
+
id: z.string().optional(),
|
|
52
|
+
name: z.string()
|
|
53
|
+
});
|
|
54
|
+
var jiraIssueTypeSchema = z.object({
|
|
55
|
+
id: z.string().optional(),
|
|
56
|
+
name: z.string(),
|
|
57
|
+
subtask: z.boolean().optional()
|
|
58
|
+
});
|
|
59
|
+
var jiraProjectReferenceSchema = z.object({
|
|
60
|
+
id: z.string().optional(),
|
|
61
|
+
key: z.string(),
|
|
62
|
+
name: z.string()
|
|
63
|
+
});
|
|
64
|
+
var jiraCommentSchema = z.object({
|
|
65
|
+
id: z.string(),
|
|
66
|
+
author: jiraUserSchema.optional(),
|
|
67
|
+
body: z.string(),
|
|
68
|
+
created: z.string().optional(),
|
|
69
|
+
updated: z.string().optional()
|
|
70
|
+
});
|
|
71
|
+
var jiraIssueSummarySchema = z.object({
|
|
72
|
+
id: z.string(),
|
|
73
|
+
key: z.string(),
|
|
74
|
+
summary: z.string(),
|
|
75
|
+
status: jiraStatusSchema.optional(),
|
|
76
|
+
assignee: jiraUserSchema.optional(),
|
|
77
|
+
reporter: jiraUserSchema.optional(),
|
|
78
|
+
priority: jiraPrioritySchema.optional(),
|
|
79
|
+
issueType: jiraIssueTypeSchema.optional(),
|
|
80
|
+
project: jiraProjectReferenceSchema.optional(),
|
|
81
|
+
created: z.string().optional(),
|
|
82
|
+
updated: z.string().optional(),
|
|
83
|
+
url: z.string().url()
|
|
84
|
+
});
|
|
85
|
+
var jiraIssueDetailSchema = jiraIssueSummarySchema.extend({
|
|
86
|
+
description: z.string().optional(),
|
|
87
|
+
labels: z.array(z.string()),
|
|
88
|
+
comments: z.array(jiraCommentSchema)
|
|
89
|
+
});
|
|
90
|
+
var jiraTransitionSchema = z.object({
|
|
91
|
+
id: z.string(),
|
|
92
|
+
name: z.string(),
|
|
93
|
+
toStatus: jiraStatusSchema.optional()
|
|
94
|
+
});
|
|
95
|
+
var jiraProjectSchema = z.object({
|
|
96
|
+
id: z.string().optional(),
|
|
97
|
+
key: z.string(),
|
|
98
|
+
name: z.string(),
|
|
99
|
+
description: z.string().optional(),
|
|
100
|
+
projectTypeKey: z.string().optional(),
|
|
101
|
+
simplified: z.boolean().optional(),
|
|
102
|
+
lead: jiraUserSchema.optional(),
|
|
103
|
+
assigneeType: z.string().optional(),
|
|
104
|
+
apiUrl: z.string().url().optional()
|
|
105
|
+
});
|
|
106
|
+
var searchIssuesInputShape = {
|
|
107
|
+
jql: z.string().trim().min(1).optional(),
|
|
108
|
+
projectKey: z.string().trim().min(1).optional(),
|
|
109
|
+
text: z.string().trim().min(1).optional(),
|
|
110
|
+
status: z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]).optional(),
|
|
111
|
+
assignee: z.string().trim().min(1).optional(),
|
|
112
|
+
maxResults: z.number().int().min(1).max(100).optional(),
|
|
113
|
+
startAt: z.number().int().min(0).optional(),
|
|
114
|
+
fields: z.array(z.string().trim().min(1)).min(1).max(50).optional()
|
|
115
|
+
};
|
|
116
|
+
var searchIssuesOutputShape = {
|
|
117
|
+
jql: z.string(),
|
|
118
|
+
startAt: z.number().int().min(0),
|
|
119
|
+
maxResults: z.number().int().min(1),
|
|
120
|
+
total: z.number().int().min(0),
|
|
121
|
+
issues: z.array(jiraIssueSummarySchema)
|
|
122
|
+
};
|
|
123
|
+
var getIssueInputShape = {
|
|
124
|
+
issueKey: z.string().trim().min(1),
|
|
125
|
+
fields: z.array(z.string().trim().min(1)).min(1).max(50).optional()
|
|
126
|
+
};
|
|
127
|
+
var getIssueOutputShape = {
|
|
128
|
+
issue: jiraIssueDetailSchema
|
|
129
|
+
};
|
|
130
|
+
var transitionIssueInputShape = {
|
|
131
|
+
issueKey: z.string().trim().min(1),
|
|
132
|
+
transitionId: z.string().trim().min(1).optional(),
|
|
133
|
+
transitionName: z.string().trim().min(1).optional(),
|
|
134
|
+
comment: z.string().trim().min(1).max(5e3).optional()
|
|
135
|
+
};
|
|
136
|
+
var transitionIssueOutputShape = {
|
|
137
|
+
issueKey: z.string(),
|
|
138
|
+
transition: jiraTransitionSchema,
|
|
139
|
+
commentAdded: z.boolean(),
|
|
140
|
+
availableTransitions: z.array(jiraTransitionSchema),
|
|
141
|
+
issue: jiraIssueDetailSchema
|
|
142
|
+
};
|
|
143
|
+
var incidentTriagePromptArgsShape = {
|
|
144
|
+
issueKey: z.string().trim().min(1).optional(),
|
|
145
|
+
projectKey: z.string().trim().min(1).optional(),
|
|
146
|
+
summary: z.string().trim().min(1),
|
|
147
|
+
symptoms: z.string().trim().min(1),
|
|
148
|
+
impact: z.string().trim().min(1).optional(),
|
|
149
|
+
suspectedService: z.string().trim().min(1).optional(),
|
|
150
|
+
environment: z.string().trim().min(1).optional()
|
|
151
|
+
};
|
|
152
|
+
var projectResourceParamsSchema = z.object({
|
|
153
|
+
projectKey: z.string().trim().min(1)
|
|
154
|
+
});
|
|
155
|
+
var jiraUserResponseSchema = z.object({
|
|
156
|
+
accountId: z.string().optional(),
|
|
157
|
+
displayName: z.string().optional(),
|
|
158
|
+
emailAddress: z.string().nullable().optional()
|
|
159
|
+
}).passthrough();
|
|
160
|
+
var jiraStatusResponseSchema = z.object({
|
|
161
|
+
id: z.string().optional(),
|
|
162
|
+
name: z.string().optional(),
|
|
163
|
+
statusCategory: z.object({
|
|
164
|
+
name: z.string().optional()
|
|
165
|
+
}).passthrough().optional()
|
|
166
|
+
}).passthrough();
|
|
167
|
+
var jiraPriorityResponseSchema = z.object({
|
|
168
|
+
id: z.string().optional(),
|
|
169
|
+
name: z.string().optional()
|
|
170
|
+
}).passthrough();
|
|
171
|
+
var jiraIssueTypeResponseSchema = z.object({
|
|
172
|
+
id: z.string().optional(),
|
|
173
|
+
name: z.string().optional(),
|
|
174
|
+
subtask: z.boolean().optional()
|
|
175
|
+
}).passthrough();
|
|
176
|
+
var jiraProjectResponseSchema = z.object({
|
|
177
|
+
id: z.string().optional(),
|
|
178
|
+
key: z.string(),
|
|
179
|
+
name: z.string(),
|
|
180
|
+
description: z.string().nullable().optional(),
|
|
181
|
+
projectTypeKey: z.string().nullable().optional(),
|
|
182
|
+
simplified: z.boolean().optional(),
|
|
183
|
+
lead: jiraUserResponseSchema.nullish().optional(),
|
|
184
|
+
assigneeType: z.string().nullable().optional(),
|
|
185
|
+
self: z.string().url().optional()
|
|
186
|
+
}).passthrough();
|
|
187
|
+
var jiraCommentResponseSchema = z.object({
|
|
188
|
+
id: z.string(),
|
|
189
|
+
author: jiraUserResponseSchema.nullish().optional(),
|
|
190
|
+
body: z.unknown().optional(),
|
|
191
|
+
created: z.string().optional(),
|
|
192
|
+
updated: z.string().optional()
|
|
193
|
+
}).passthrough();
|
|
194
|
+
var jiraIssueFieldsResponseSchema = z.object({
|
|
195
|
+
summary: z.string().nullable().optional(),
|
|
196
|
+
description: z.unknown().optional(),
|
|
197
|
+
status: jiraStatusResponseSchema.nullish().optional(),
|
|
198
|
+
assignee: jiraUserResponseSchema.nullish().optional(),
|
|
199
|
+
reporter: jiraUserResponseSchema.nullish().optional(),
|
|
200
|
+
priority: jiraPriorityResponseSchema.nullish().optional(),
|
|
201
|
+
issuetype: jiraIssueTypeResponseSchema.nullish().optional(),
|
|
202
|
+
project: jiraProjectResponseSchema.nullish().optional(),
|
|
203
|
+
labels: z.array(z.string()).optional(),
|
|
204
|
+
comment: z.object({
|
|
205
|
+
comments: z.array(jiraCommentResponseSchema)
|
|
206
|
+
}).nullable().optional(),
|
|
207
|
+
created: z.string().optional(),
|
|
208
|
+
updated: z.string().optional()
|
|
209
|
+
}).passthrough();
|
|
210
|
+
var jiraIssueResponseSchema = z.object({
|
|
211
|
+
id: z.string(),
|
|
212
|
+
key: z.string(),
|
|
213
|
+
fields: jiraIssueFieldsResponseSchema
|
|
214
|
+
}).passthrough();
|
|
215
|
+
var jiraSearchResponseSchema = z.object({
|
|
216
|
+
startAt: z.number().int().min(0),
|
|
217
|
+
maxResults: z.number().int().min(0),
|
|
218
|
+
total: z.number().int().min(0),
|
|
219
|
+
issues: z.array(jiraIssueResponseSchema)
|
|
220
|
+
}).passthrough();
|
|
221
|
+
var jiraTransitionsResponseSchema = z.object({
|
|
222
|
+
transitions: z.array(
|
|
223
|
+
z.object({
|
|
224
|
+
id: z.string(),
|
|
225
|
+
name: z.string(),
|
|
226
|
+
to: jiraStatusResponseSchema.nullish().optional()
|
|
227
|
+
}).passthrough()
|
|
228
|
+
)
|
|
229
|
+
}).passthrough();
|
|
230
|
+
var jiraErrorResponseSchema = z.object({
|
|
231
|
+
errorMessages: z.array(z.string()).optional(),
|
|
232
|
+
errors: z.record(z.string(), z.string()).optional(),
|
|
233
|
+
message: z.string().optional()
|
|
234
|
+
}).passthrough();
|
|
235
|
+
function createBasicAuthHeader(email, apiToken) {
|
|
236
|
+
return `Basic ${Buffer.from(`${email}:${apiToken}`, "utf8").toString("base64")}`;
|
|
237
|
+
}
|
|
238
|
+
function appendErrorDetail(message, detail) {
|
|
239
|
+
const trimmedDetail = detail?.trim();
|
|
240
|
+
if (!trimmedDetail) {
|
|
241
|
+
return message;
|
|
242
|
+
}
|
|
243
|
+
return `${message} ${trimmedDetail}`;
|
|
244
|
+
}
|
|
245
|
+
function escapeJqlValue(value) {
|
|
246
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
247
|
+
}
|
|
248
|
+
function quoteJqlValue(value) {
|
|
249
|
+
return `"${escapeJqlValue(value)}"`;
|
|
250
|
+
}
|
|
251
|
+
function formatStatusClause(status) {
|
|
252
|
+
if (typeof status === "string") {
|
|
253
|
+
return `status = ${quoteJqlValue(status)}`;
|
|
254
|
+
}
|
|
255
|
+
return `status in (${status.map((value) => quoteJqlValue(value)).join(", ")})`;
|
|
256
|
+
}
|
|
257
|
+
function createIssueBrowseUrl(baseUrl, issueKey) {
|
|
258
|
+
return new URL(`browse/${encodeURIComponent(issueKey)}`, `${baseUrl}/`).toString();
|
|
259
|
+
}
|
|
260
|
+
function createProjectResourceUri(projectKey) {
|
|
261
|
+
return `jira://projects/${encodeURIComponent(projectKey)}`;
|
|
262
|
+
}
|
|
263
|
+
function createJiraDocument(text) {
|
|
264
|
+
return {
|
|
265
|
+
type: "doc",
|
|
266
|
+
version: 1,
|
|
267
|
+
content: [
|
|
268
|
+
{
|
|
269
|
+
type: "paragraph",
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: "text",
|
|
273
|
+
text
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function collectDocumentText(value) {
|
|
281
|
+
if (typeof value === "string") {
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(value)) {
|
|
285
|
+
return value.map((entry) => collectDocumentText(entry)).join("");
|
|
286
|
+
}
|
|
287
|
+
if (!value || typeof value !== "object") {
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
const record = value;
|
|
291
|
+
const nodeType = typeof record.type === "string" ? record.type : void 0;
|
|
292
|
+
const directText = typeof record.text === "string" ? record.text : "";
|
|
293
|
+
const contentText = Array.isArray(record.content) ? record.content.map((entry) => collectDocumentText(entry)).join("") : "";
|
|
294
|
+
const combinedText = `${directText}${contentText}`;
|
|
295
|
+
switch (nodeType) {
|
|
296
|
+
case "bulletList":
|
|
297
|
+
case "heading":
|
|
298
|
+
case "listItem":
|
|
299
|
+
case "orderedList":
|
|
300
|
+
case "paragraph":
|
|
301
|
+
return `${combinedText}
|
|
302
|
+
`;
|
|
303
|
+
case "hardBreak":
|
|
304
|
+
return "\n";
|
|
305
|
+
default:
|
|
306
|
+
return combinedText;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function extractText(value) {
|
|
310
|
+
const text = collectDocumentText(value).replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
311
|
+
return text.length > 0 ? text : void 0;
|
|
312
|
+
}
|
|
313
|
+
function formatJiraApiError(body, fallbackText) {
|
|
314
|
+
const parsed = jiraErrorResponseSchema.safeParse(body);
|
|
315
|
+
const messages = [];
|
|
316
|
+
if (parsed.success) {
|
|
317
|
+
if (parsed.data.message?.trim()) {
|
|
318
|
+
messages.push(parsed.data.message.trim());
|
|
319
|
+
}
|
|
320
|
+
for (const message of parsed.data.errorMessages ?? []) {
|
|
321
|
+
if (message.trim()) {
|
|
322
|
+
messages.push(message.trim());
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (parsed.data.errors) {
|
|
326
|
+
for (const [field, message] of Object.entries(parsed.data.errors)) {
|
|
327
|
+
const trimmedMessage = message.trim();
|
|
328
|
+
if (trimmedMessage) {
|
|
329
|
+
messages.push(`${field}: ${trimmedMessage}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const trimmedFallback = fallbackText.trim();
|
|
335
|
+
if (messages.length > 0) {
|
|
336
|
+
return messages.join("; ");
|
|
337
|
+
}
|
|
338
|
+
return trimmedFallback.length > 0 ? trimmedFallback : void 0;
|
|
339
|
+
}
|
|
340
|
+
function normalizeUser(user) {
|
|
341
|
+
if (!user) {
|
|
342
|
+
return void 0;
|
|
343
|
+
}
|
|
344
|
+
const displayName = user.displayName?.trim() || user.emailAddress?.trim() || user.accountId?.trim();
|
|
345
|
+
if (!displayName) {
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
const normalized = {
|
|
349
|
+
displayName
|
|
350
|
+
};
|
|
351
|
+
if (user.accountId?.trim()) {
|
|
352
|
+
normalized.accountId = user.accountId.trim();
|
|
353
|
+
}
|
|
354
|
+
if (user.emailAddress !== void 0) {
|
|
355
|
+
normalized.emailAddress = user.emailAddress;
|
|
356
|
+
}
|
|
357
|
+
return normalized;
|
|
358
|
+
}
|
|
359
|
+
function normalizeStatus(status) {
|
|
360
|
+
if (!status?.name?.trim()) {
|
|
361
|
+
return void 0;
|
|
362
|
+
}
|
|
363
|
+
const normalized = {
|
|
364
|
+
name: status.name.trim()
|
|
365
|
+
};
|
|
366
|
+
if (status.id?.trim()) {
|
|
367
|
+
normalized.id = status.id.trim();
|
|
368
|
+
}
|
|
369
|
+
if (status.statusCategory?.name?.trim()) {
|
|
370
|
+
normalized.category = status.statusCategory.name.trim();
|
|
371
|
+
}
|
|
372
|
+
return normalized;
|
|
373
|
+
}
|
|
374
|
+
function normalizePriority(priority) {
|
|
375
|
+
if (!priority?.name?.trim()) {
|
|
376
|
+
return void 0;
|
|
377
|
+
}
|
|
378
|
+
const normalized = {
|
|
379
|
+
name: priority.name.trim()
|
|
380
|
+
};
|
|
381
|
+
if (priority.id?.trim()) {
|
|
382
|
+
normalized.id = priority.id.trim();
|
|
383
|
+
}
|
|
384
|
+
return normalized;
|
|
385
|
+
}
|
|
386
|
+
function normalizeIssueType(issueType) {
|
|
387
|
+
if (!issueType?.name?.trim()) {
|
|
388
|
+
return void 0;
|
|
389
|
+
}
|
|
390
|
+
const normalized = {
|
|
391
|
+
name: issueType.name.trim()
|
|
392
|
+
};
|
|
393
|
+
if (issueType.id?.trim()) {
|
|
394
|
+
normalized.id = issueType.id.trim();
|
|
395
|
+
}
|
|
396
|
+
if (issueType.subtask !== void 0) {
|
|
397
|
+
normalized.subtask = issueType.subtask;
|
|
398
|
+
}
|
|
399
|
+
return normalized;
|
|
400
|
+
}
|
|
401
|
+
function normalizeProjectReference(project) {
|
|
402
|
+
if (!project) {
|
|
403
|
+
return void 0;
|
|
404
|
+
}
|
|
405
|
+
const normalized = {
|
|
406
|
+
key: project.key,
|
|
407
|
+
name: project.name
|
|
408
|
+
};
|
|
409
|
+
if (project.id?.trim()) {
|
|
410
|
+
normalized.id = project.id.trim();
|
|
411
|
+
}
|
|
412
|
+
return normalized;
|
|
413
|
+
}
|
|
414
|
+
function normalizeComment(comment) {
|
|
415
|
+
const normalized = {
|
|
416
|
+
id: comment.id,
|
|
417
|
+
body: extractText(comment.body) ?? ""
|
|
418
|
+
};
|
|
419
|
+
const author = normalizeUser(comment.author);
|
|
420
|
+
if (author) {
|
|
421
|
+
normalized.author = author;
|
|
422
|
+
}
|
|
423
|
+
if (comment.created?.trim()) {
|
|
424
|
+
normalized.created = comment.created.trim();
|
|
425
|
+
}
|
|
426
|
+
if (comment.updated?.trim()) {
|
|
427
|
+
normalized.updated = comment.updated.trim();
|
|
428
|
+
}
|
|
429
|
+
return normalized;
|
|
430
|
+
}
|
|
431
|
+
function normalizeIssueSummary(issue, baseUrl) {
|
|
432
|
+
const normalized = {
|
|
433
|
+
id: issue.id,
|
|
434
|
+
key: issue.key,
|
|
435
|
+
summary: issue.fields.summary?.trim() || issue.key,
|
|
436
|
+
url: createIssueBrowseUrl(baseUrl, issue.key)
|
|
437
|
+
};
|
|
438
|
+
const status = normalizeStatus(issue.fields.status);
|
|
439
|
+
if (status) {
|
|
440
|
+
normalized.status = status;
|
|
441
|
+
}
|
|
442
|
+
const assignee = normalizeUser(issue.fields.assignee);
|
|
443
|
+
if (assignee) {
|
|
444
|
+
normalized.assignee = assignee;
|
|
445
|
+
}
|
|
446
|
+
const reporter = normalizeUser(issue.fields.reporter);
|
|
447
|
+
if (reporter) {
|
|
448
|
+
normalized.reporter = reporter;
|
|
449
|
+
}
|
|
450
|
+
const priority = normalizePriority(issue.fields.priority);
|
|
451
|
+
if (priority) {
|
|
452
|
+
normalized.priority = priority;
|
|
453
|
+
}
|
|
454
|
+
const issueType = normalizeIssueType(issue.fields.issuetype);
|
|
455
|
+
if (issueType) {
|
|
456
|
+
normalized.issueType = issueType;
|
|
457
|
+
}
|
|
458
|
+
const project = normalizeProjectReference(issue.fields.project);
|
|
459
|
+
if (project) {
|
|
460
|
+
normalized.project = project;
|
|
461
|
+
}
|
|
462
|
+
if (issue.fields.created?.trim()) {
|
|
463
|
+
normalized.created = issue.fields.created.trim();
|
|
464
|
+
}
|
|
465
|
+
if (issue.fields.updated?.trim()) {
|
|
466
|
+
normalized.updated = issue.fields.updated.trim();
|
|
467
|
+
}
|
|
468
|
+
return normalized;
|
|
469
|
+
}
|
|
470
|
+
function normalizeIssueDetail(issue, baseUrl) {
|
|
471
|
+
const normalized = {
|
|
472
|
+
...normalizeIssueSummary(issue, baseUrl),
|
|
473
|
+
labels: issue.fields.labels ?? [],
|
|
474
|
+
comments: (issue.fields.comment?.comments ?? []).map((comment) => normalizeComment(comment))
|
|
475
|
+
};
|
|
476
|
+
const description = extractText(issue.fields.description);
|
|
477
|
+
if (description) {
|
|
478
|
+
normalized.description = description;
|
|
479
|
+
}
|
|
480
|
+
return normalized;
|
|
481
|
+
}
|
|
482
|
+
function normalizeTransition(transition) {
|
|
483
|
+
const normalized = {
|
|
484
|
+
id: transition.id,
|
|
485
|
+
name: transition.name
|
|
486
|
+
};
|
|
487
|
+
const toStatus = normalizeStatus(transition.to);
|
|
488
|
+
if (toStatus) {
|
|
489
|
+
normalized.toStatus = toStatus;
|
|
490
|
+
}
|
|
491
|
+
return normalized;
|
|
492
|
+
}
|
|
493
|
+
function normalizeProject(project) {
|
|
494
|
+
const normalized = {
|
|
495
|
+
key: project.key,
|
|
496
|
+
name: project.name
|
|
497
|
+
};
|
|
498
|
+
if (project.id?.trim()) {
|
|
499
|
+
normalized.id = project.id.trim();
|
|
500
|
+
}
|
|
501
|
+
if (project.description?.trim()) {
|
|
502
|
+
normalized.description = project.description.trim();
|
|
503
|
+
}
|
|
504
|
+
if (project.projectTypeKey?.trim()) {
|
|
505
|
+
normalized.projectTypeKey = project.projectTypeKey.trim();
|
|
506
|
+
}
|
|
507
|
+
if (project.simplified !== void 0) {
|
|
508
|
+
normalized.simplified = project.simplified;
|
|
509
|
+
}
|
|
510
|
+
const lead = normalizeUser(project.lead);
|
|
511
|
+
if (lead) {
|
|
512
|
+
normalized.lead = lead;
|
|
513
|
+
}
|
|
514
|
+
if (project.assigneeType?.trim()) {
|
|
515
|
+
normalized.assigneeType = project.assigneeType.trim();
|
|
516
|
+
}
|
|
517
|
+
if (project.self?.trim()) {
|
|
518
|
+
normalized.apiUrl = project.self.trim();
|
|
519
|
+
}
|
|
520
|
+
return normalized;
|
|
521
|
+
}
|
|
522
|
+
function buildSearchJql(input, defaultProjectKey) {
|
|
523
|
+
const structuredClauses = [];
|
|
524
|
+
const effectiveProjectKey = input.jql ? input.projectKey : input.projectKey ?? defaultProjectKey;
|
|
525
|
+
if (effectiveProjectKey) {
|
|
526
|
+
structuredClauses.push(`project = ${quoteJqlValue(effectiveProjectKey)}`);
|
|
527
|
+
}
|
|
528
|
+
if (input.status) {
|
|
529
|
+
structuredClauses.push(formatStatusClause(input.status));
|
|
530
|
+
}
|
|
531
|
+
if (input.assignee) {
|
|
532
|
+
structuredClauses.push(`assignee = ${quoteJqlValue(input.assignee)}`);
|
|
533
|
+
}
|
|
534
|
+
if (input.text) {
|
|
535
|
+
structuredClauses.push(`text ~ ${quoteJqlValue(input.text)}`);
|
|
536
|
+
}
|
|
537
|
+
const hasOrderByInJql = input.jql ? /\border\s+by\b/i.test(input.jql) : false;
|
|
538
|
+
if (input.jql && hasOrderByInJql && structuredClauses.length > 0) {
|
|
539
|
+
throw new ValidationError(
|
|
540
|
+
"Do not combine structured search filters with a JQL query that already contains ORDER BY."
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
let jql = input.jql?.trim() ?? "";
|
|
544
|
+
if (jql && structuredClauses.length > 0) {
|
|
545
|
+
jql = `(${jql}) AND ${structuredClauses.join(" AND ")}`;
|
|
546
|
+
} else if (!jql) {
|
|
547
|
+
jql = structuredClauses.join(" AND ");
|
|
548
|
+
}
|
|
549
|
+
if (!jql) {
|
|
550
|
+
throw new ValidationError(
|
|
551
|
+
`Provide JQL or at least one search filter, or configure ${OPTIONAL_ENV_VAR_NAMES[0]} for a default project.`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
if (!/\border\s+by\b/i.test(jql)) {
|
|
555
|
+
jql = `${jql} ORDER BY updated DESC`;
|
|
556
|
+
}
|
|
557
|
+
return jql;
|
|
558
|
+
}
|
|
559
|
+
function createSearchRequest(input, environment) {
|
|
560
|
+
const fields = input.fields ?? DEFAULT_SEARCH_FIELDS;
|
|
561
|
+
return {
|
|
562
|
+
jql: buildSearchJql(input, environment.defaultProjectKey),
|
|
563
|
+
startAt: input.startAt ?? 0,
|
|
564
|
+
maxResults: input.maxResults ?? 10,
|
|
565
|
+
fields
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
function renderSearchIssues(output) {
|
|
569
|
+
const header = `Found ${output.issues.length} of ${output.total} Jira issues.`;
|
|
570
|
+
const jql = `JQL: ${output.jql}`;
|
|
571
|
+
const issues = output.issues.map((issue) => {
|
|
572
|
+
const status = issue.status?.name ?? "Unknown";
|
|
573
|
+
return `- ${issue.key}: ${issue.summary} (${status})`;
|
|
574
|
+
});
|
|
575
|
+
return [header, jql, ...issues].join("\n");
|
|
576
|
+
}
|
|
577
|
+
function renderIssueDetail(output) {
|
|
578
|
+
const { issue } = output;
|
|
579
|
+
const lines = [
|
|
580
|
+
`${issue.key}: ${issue.summary}`,
|
|
581
|
+
`Status: ${issue.status?.name ?? "Unknown"}`,
|
|
582
|
+
`Assignee: ${issue.assignee?.displayName ?? "Unassigned"}`,
|
|
583
|
+
`Reporter: ${issue.reporter?.displayName ?? "Unknown"}`,
|
|
584
|
+
`Priority: ${issue.priority?.name ?? "Unknown"}`,
|
|
585
|
+
`Project: ${issue.project?.key ?? "Unknown"}`
|
|
586
|
+
];
|
|
587
|
+
if (issue.description) {
|
|
588
|
+
lines.push("", issue.description);
|
|
589
|
+
}
|
|
590
|
+
return lines.join("\n");
|
|
591
|
+
}
|
|
592
|
+
function renderTransitionResult(output) {
|
|
593
|
+
return [
|
|
594
|
+
`Transitioned ${output.issueKey} with "${output.transition.name}".`,
|
|
595
|
+
`New status: ${output.issue.status?.name ?? output.transition.toStatus?.name ?? "Unknown"}`,
|
|
596
|
+
`Comment added: ${output.commentAdded ? "yes" : "no"}`
|
|
597
|
+
].join("\n");
|
|
598
|
+
}
|
|
599
|
+
function ensureTransitionInput(input) {
|
|
600
|
+
if (!input.transitionId && !input.transitionName) {
|
|
601
|
+
throw new ValidationError("Provide either transitionId or transitionName.");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function resolveTransition(transitions, input, issueKey) {
|
|
605
|
+
const availableTransitions = transitions.map((transition) => transition.name).join(", ");
|
|
606
|
+
const transitionById = input.transitionId ? transitions.find((transition) => transition.id === input.transitionId) : void 0;
|
|
607
|
+
const transitionByName = input.transitionName ? transitions.find((transition) => transition.name.toLowerCase() === input.transitionName?.toLowerCase()) : void 0;
|
|
608
|
+
if (input.transitionId && !transitionById) {
|
|
609
|
+
throw new ValidationError(
|
|
610
|
+
appendErrorDetail(
|
|
611
|
+
`Transition id '${input.transitionId}' is not available for issue '${issueKey}'.`,
|
|
612
|
+
availableTransitions ? `Available transitions: ${availableTransitions}.` : void 0
|
|
613
|
+
)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (input.transitionName && !transitionByName) {
|
|
617
|
+
throw new ValidationError(
|
|
618
|
+
appendErrorDetail(
|
|
619
|
+
`Transition '${input.transitionName}' is not available for issue '${issueKey}'.`,
|
|
620
|
+
availableTransitions ? `Available transitions: ${availableTransitions}.` : void 0
|
|
621
|
+
)
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (transitionById && transitionByName && transitionById.id !== transitionByName.id) {
|
|
625
|
+
throw new ValidationError("transitionId and transitionName refer to different Jira transitions.");
|
|
626
|
+
}
|
|
627
|
+
const selectedTransition = transitionById ?? transitionByName;
|
|
628
|
+
if (!selectedTransition) {
|
|
629
|
+
throw new ValidationError(
|
|
630
|
+
appendErrorDetail(
|
|
631
|
+
`No matching transition is available for issue '${issueKey}'.`,
|
|
632
|
+
availableTransitions ? `Available transitions: ${availableTransitions}.` : void 0
|
|
633
|
+
)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return selectedTransition;
|
|
637
|
+
}
|
|
638
|
+
function rethrowJiraOperationError(error, message) {
|
|
639
|
+
if (error instanceof ValidationError) {
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
if (error instanceof ExternalServiceError) {
|
|
643
|
+
throw new ExternalServiceError(appendErrorDetail(message, error.message), {
|
|
644
|
+
statusCode: error.statusCode,
|
|
645
|
+
details: error.details,
|
|
646
|
+
exposeToClient: error.exposeToClient
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (error instanceof Error) {
|
|
650
|
+
throw new ExternalServiceError(appendErrorDetail(message, error.message), {
|
|
651
|
+
details: error.stack
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
throw new ExternalServiceError(message, {
|
|
655
|
+
details: error,
|
|
656
|
+
exposeToClient: false
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function loadJiraEnvironment(source = process.env) {
|
|
660
|
+
const env = loadEnv(jiraEnvironmentShape, source);
|
|
661
|
+
return {
|
|
662
|
+
baseUrl: env.JIRA_BASE_URL,
|
|
663
|
+
email: env.JIRA_EMAIL,
|
|
664
|
+
apiToken: env.JIRA_API_TOKEN,
|
|
665
|
+
...env.JIRA_DEFAULT_PROJECT_KEY ? { defaultProjectKey: env.JIRA_DEFAULT_PROJECT_KEY } : {}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
var JiraRestClient = class {
|
|
669
|
+
environment;
|
|
670
|
+
fetchImpl;
|
|
671
|
+
constructor(options) {
|
|
672
|
+
this.environment = options.environment;
|
|
673
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
674
|
+
}
|
|
675
|
+
async searchIssues(request) {
|
|
676
|
+
const response = await this.requestJson("/rest/api/3/search", jiraSearchResponseSchema, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
operation: "searching Jira issues",
|
|
679
|
+
badRequestMessage: "Jira rejected the issue search request.",
|
|
680
|
+
body: {
|
|
681
|
+
jql: request.jql,
|
|
682
|
+
startAt: request.startAt,
|
|
683
|
+
maxResults: request.maxResults,
|
|
684
|
+
fields: [...request.fields]
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
return {
|
|
688
|
+
startAt: response.startAt,
|
|
689
|
+
maxResults: response.maxResults,
|
|
690
|
+
total: response.total,
|
|
691
|
+
issues: response.issues.map((issue) => normalizeIssueSummary(issue, this.environment.baseUrl))
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async getIssue(issueKey, fields = DEFAULT_ISSUE_FIELDS) {
|
|
695
|
+
const response = await this.requestJson(
|
|
696
|
+
`/rest/api/3/issue/${encodeURIComponent(issueKey)}`,
|
|
697
|
+
jiraIssueResponseSchema,
|
|
698
|
+
{
|
|
699
|
+
method: "GET",
|
|
700
|
+
operation: `fetching Jira issue '${issueKey}'`,
|
|
701
|
+
notFoundMessage: `Issue '${issueKey}' was not found in Jira.`,
|
|
702
|
+
badRequestMessage: `Jira could not fetch issue '${issueKey}'.`,
|
|
703
|
+
query: {
|
|
704
|
+
fields: fields.join(",")
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
);
|
|
708
|
+
return normalizeIssueDetail(response, this.environment.baseUrl);
|
|
709
|
+
}
|
|
710
|
+
async getTransitions(issueKey) {
|
|
711
|
+
const response = await this.requestJson(
|
|
712
|
+
`/rest/api/3/issue/${encodeURIComponent(issueKey)}/transitions`,
|
|
713
|
+
jiraTransitionsResponseSchema,
|
|
714
|
+
{
|
|
715
|
+
method: "GET",
|
|
716
|
+
operation: `listing transitions for Jira issue '${issueKey}'`,
|
|
717
|
+
notFoundMessage: `Issue '${issueKey}' was not found in Jira.`,
|
|
718
|
+
badRequestMessage: `Jira could not list transitions for issue '${issueKey}'.`
|
|
719
|
+
}
|
|
720
|
+
);
|
|
721
|
+
return response.transitions.map((transition) => normalizeTransition(transition));
|
|
722
|
+
}
|
|
723
|
+
async transitionIssue(issueKey, transitionId, comment) {
|
|
724
|
+
const body = {
|
|
725
|
+
transition: {
|
|
726
|
+
id: transitionId
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
if (comment) {
|
|
730
|
+
body.update = {
|
|
731
|
+
comment: [
|
|
732
|
+
{
|
|
733
|
+
add: {
|
|
734
|
+
body: createJiraDocument(comment)
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
await this.requestVoid(`/rest/api/3/issue/${encodeURIComponent(issueKey)}/transitions`, {
|
|
741
|
+
method: "POST",
|
|
742
|
+
operation: `transitioning Jira issue '${issueKey}'`,
|
|
743
|
+
notFoundMessage: `Issue '${issueKey}' was not found in Jira.`,
|
|
744
|
+
badRequestMessage: `Jira rejected the transition request for issue '${issueKey}'.`,
|
|
745
|
+
body
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
async getProject(projectKey) {
|
|
749
|
+
const response = await this.requestJson(
|
|
750
|
+
`/rest/api/3/project/${encodeURIComponent(projectKey)}`,
|
|
751
|
+
jiraProjectResponseSchema,
|
|
752
|
+
{
|
|
753
|
+
method: "GET",
|
|
754
|
+
operation: `fetching Jira project '${projectKey}'`,
|
|
755
|
+
notFoundMessage: `Project '${projectKey}' was not found in Jira.`,
|
|
756
|
+
badRequestMessage: `Jira could not fetch project '${projectKey}'.`
|
|
757
|
+
}
|
|
758
|
+
);
|
|
759
|
+
return normalizeProject(response);
|
|
760
|
+
}
|
|
761
|
+
createUrl(path, query) {
|
|
762
|
+
const url = new URL(path.startsWith("/") ? path.slice(1) : path, `${this.environment.baseUrl}/`);
|
|
763
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
764
|
+
if (value !== void 0) {
|
|
765
|
+
url.searchParams.set(key, String(value));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return url;
|
|
769
|
+
}
|
|
770
|
+
createHeaders(hasJsonBody) {
|
|
771
|
+
const headers = new Headers();
|
|
772
|
+
headers.set("accept", "application/json");
|
|
773
|
+
headers.set("authorization", createBasicAuthHeader(this.environment.email, this.environment.apiToken));
|
|
774
|
+
if (hasJsonBody) {
|
|
775
|
+
headers.set("content-type", "application/json");
|
|
776
|
+
}
|
|
777
|
+
return headers;
|
|
778
|
+
}
|
|
779
|
+
async requestJson(path, schema, options) {
|
|
780
|
+
const response = await this.request(path, options);
|
|
781
|
+
const bodyText = await response.text();
|
|
782
|
+
if (!bodyText.trim()) {
|
|
783
|
+
throw new ExternalServiceError(appendErrorDetail(`Jira returned an empty response while ${options.operation}.`));
|
|
784
|
+
}
|
|
785
|
+
const payload = this.tryParseJson(bodyText);
|
|
786
|
+
if (payload === void 0) {
|
|
787
|
+
throw new ExternalServiceError(
|
|
788
|
+
`Jira returned malformed JSON while ${options.operation}.`,
|
|
789
|
+
{
|
|
790
|
+
details: bodyText
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
const parsed = schema.safeParse(payload);
|
|
795
|
+
if (!parsed.success) {
|
|
796
|
+
throw new ExternalServiceError(`Jira returned an unexpected response while ${options.operation}.`, {
|
|
797
|
+
details: parsed.error.flatten()
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
return parsed.data;
|
|
801
|
+
}
|
|
802
|
+
async requestVoid(path, options) {
|
|
803
|
+
const response = await this.request(path, options);
|
|
804
|
+
if (response.status === 204) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const bodyText = await response.text();
|
|
808
|
+
if (bodyText.trim().length > 0) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async request(path, options) {
|
|
813
|
+
const url = this.createUrl(path, options.query);
|
|
814
|
+
const hasBody = options.body !== void 0;
|
|
815
|
+
const requestInit = {
|
|
816
|
+
method: options.method,
|
|
817
|
+
headers: this.createHeaders(hasBody)
|
|
818
|
+
};
|
|
819
|
+
if (hasBody) {
|
|
820
|
+
requestInit.body = JSON.stringify(options.body);
|
|
821
|
+
}
|
|
822
|
+
let response;
|
|
823
|
+
try {
|
|
824
|
+
response = await this.fetchImpl(url, requestInit);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (error instanceof Error) {
|
|
827
|
+
throw new ExternalServiceError(
|
|
828
|
+
appendErrorDetail(`Failed to reach Jira while ${options.operation}.`, error.message),
|
|
829
|
+
{
|
|
830
|
+
details: error.stack
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
throw new ExternalServiceError(`Failed to reach Jira while ${options.operation}.`, {
|
|
835
|
+
details: error,
|
|
836
|
+
exposeToClient: false
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
if (!response.ok) {
|
|
840
|
+
const bodyText = await response.text();
|
|
841
|
+
const body = bodyText.trim() ? this.tryParseJson(bodyText) : void 0;
|
|
842
|
+
const detail = formatJiraApiError(body, bodyText);
|
|
843
|
+
switch (response.status) {
|
|
844
|
+
case 400:
|
|
845
|
+
throw new ValidationError(
|
|
846
|
+
appendErrorDetail(options.badRequestMessage ?? "Jira rejected the request.", detail),
|
|
847
|
+
body ?? bodyText
|
|
848
|
+
);
|
|
849
|
+
case 401:
|
|
850
|
+
throw new ExternalServiceError(
|
|
851
|
+
appendErrorDetail(
|
|
852
|
+
"Jira authentication failed. Check JIRA_EMAIL and JIRA_API_TOKEN.",
|
|
853
|
+
detail
|
|
854
|
+
),
|
|
855
|
+
{
|
|
856
|
+
statusCode: response.status,
|
|
857
|
+
details: body ?? bodyText
|
|
858
|
+
}
|
|
859
|
+
);
|
|
860
|
+
case 403:
|
|
861
|
+
throw new ExternalServiceError(
|
|
862
|
+
appendErrorDetail("Jira denied access to the requested resource.", detail),
|
|
863
|
+
{
|
|
864
|
+
statusCode: response.status,
|
|
865
|
+
details: body ?? bodyText
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
case 404:
|
|
869
|
+
throw new ExternalServiceError(
|
|
870
|
+
appendErrorDetail(options.notFoundMessage ?? "The requested Jira resource was not found.", detail),
|
|
871
|
+
{
|
|
872
|
+
statusCode: response.status,
|
|
873
|
+
details: body ?? bodyText
|
|
874
|
+
}
|
|
875
|
+
);
|
|
876
|
+
case 429:
|
|
877
|
+
throw new ExternalServiceError(
|
|
878
|
+
appendErrorDetail("Jira rate limited the request. Retry later.", detail),
|
|
879
|
+
{
|
|
880
|
+
statusCode: response.status,
|
|
881
|
+
details: body ?? bodyText
|
|
882
|
+
}
|
|
883
|
+
);
|
|
884
|
+
default:
|
|
885
|
+
throw new ExternalServiceError(
|
|
886
|
+
appendErrorDetail(
|
|
887
|
+
`Jira request failed with status ${response.status} while ${options.operation}.`,
|
|
888
|
+
detail
|
|
889
|
+
),
|
|
890
|
+
{
|
|
891
|
+
statusCode: response.status,
|
|
892
|
+
details: body ?? bodyText
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return response;
|
|
898
|
+
}
|
|
899
|
+
tryParseJson(text) {
|
|
900
|
+
try {
|
|
901
|
+
return JSON.parse(text);
|
|
902
|
+
} catch {
|
|
903
|
+
return void 0;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
var metadata = {
|
|
908
|
+
id: "jira",
|
|
909
|
+
title: "Jira MCP Server",
|
|
910
|
+
description: "Search Jira Cloud issues, inspect tickets, transition work, and fetch project context.",
|
|
911
|
+
version: SERVER_VERSION,
|
|
912
|
+
packageName: PACKAGE_NAME,
|
|
913
|
+
homepage: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit#readme",
|
|
914
|
+
repositoryUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
|
|
915
|
+
documentationUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit/tree/main/servers/jira",
|
|
916
|
+
envVarNames: REQUIRED_ENV_VAR_NAMES,
|
|
917
|
+
transports: ["stdio", "sse"],
|
|
918
|
+
toolNames: TOOL_NAMES,
|
|
919
|
+
resourceNames: RESOURCE_NAMES,
|
|
920
|
+
promptNames: PROMPT_NAMES
|
|
921
|
+
};
|
|
922
|
+
var serverCard = createServerCard(metadata);
|
|
923
|
+
var JiraServer = class extends ToolkitServer {
|
|
924
|
+
client;
|
|
925
|
+
environment;
|
|
926
|
+
constructor(options = {}) {
|
|
927
|
+
const environment = options.environment ?? loadJiraEnvironment(options.envSource);
|
|
928
|
+
super(metadata);
|
|
929
|
+
this.environment = environment;
|
|
930
|
+
this.client = options.client ?? new JiraRestClient({ environment });
|
|
931
|
+
this.registerTool(this.createGetIssueTool());
|
|
932
|
+
this.registerTool(this.createSearchIssuesTool());
|
|
933
|
+
this.registerTool(this.createTransitionIssueTool());
|
|
934
|
+
this.registerProjectResource();
|
|
935
|
+
this.registerIncidentTriagePrompt();
|
|
936
|
+
}
|
|
937
|
+
async readProjectResource(projectKey, uri = createProjectResourceUri(projectKey)) {
|
|
938
|
+
try {
|
|
939
|
+
const project = await this.client.getProject(projectKey);
|
|
940
|
+
return this.createJsonResource(uri, {
|
|
941
|
+
project
|
|
942
|
+
});
|
|
943
|
+
} catch (error) {
|
|
944
|
+
rethrowJiraOperationError(error, `Failed to read Jira project resource for '${projectKey}'.`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
buildIncidentTriagePrompt(args) {
|
|
948
|
+
const effectiveProjectKey = args.projectKey ?? this.environment.defaultProjectKey;
|
|
949
|
+
const projectResourceHint = effectiveProjectKey ? `Project resource: ${createProjectResourceUri(effectiveProjectKey)}` : "Project resource: provide a projectKey if you want project context.";
|
|
950
|
+
const contextLines = [
|
|
951
|
+
`Issue key: ${args.issueKey ?? "not provided"}`,
|
|
952
|
+
`Project key: ${effectiveProjectKey ?? "not provided"}`,
|
|
953
|
+
`Summary: ${args.summary}`,
|
|
954
|
+
`Symptoms: ${args.symptoms}`,
|
|
955
|
+
`Impact: ${args.impact ?? "not provided"}`,
|
|
956
|
+
`Suspected service: ${args.suspectedService ?? "not provided"}`,
|
|
957
|
+
`Environment: ${args.environment ?? "not provided"}`,
|
|
958
|
+
projectResourceHint
|
|
959
|
+
];
|
|
960
|
+
return {
|
|
961
|
+
messages: [
|
|
962
|
+
{
|
|
963
|
+
role: "assistant",
|
|
964
|
+
content: {
|
|
965
|
+
type: "text",
|
|
966
|
+
text: [
|
|
967
|
+
"You are helping triage a Jira incident.",
|
|
968
|
+
"Respond with:",
|
|
969
|
+
"1. Estimated severity and blast radius.",
|
|
970
|
+
"2. Immediate mitigation steps.",
|
|
971
|
+
"3. Investigation plan with evidence to gather.",
|
|
972
|
+
"4. Recommended Jira updates, including status, owner, and next comment."
|
|
973
|
+
].join("\n")
|
|
974
|
+
}
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
role: "user",
|
|
978
|
+
content: {
|
|
979
|
+
type: "text",
|
|
980
|
+
text: contextLines.join("\n")
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
]
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
createSearchIssuesTool() {
|
|
987
|
+
return defineTool({
|
|
988
|
+
name: "search_issues",
|
|
989
|
+
title: "Search Jira issues",
|
|
990
|
+
description: "Search Jira issues with JQL or structured filters and return normalized issue summaries.",
|
|
991
|
+
inputSchema: searchIssuesInputShape,
|
|
992
|
+
outputSchema: searchIssuesOutputShape,
|
|
993
|
+
handler: async (input, context) => {
|
|
994
|
+
const request = createSearchRequest(input, this.environment);
|
|
995
|
+
await context.log("info", `Searching Jira with JQL: ${request.jql}`);
|
|
996
|
+
try {
|
|
997
|
+
const result = await this.client.searchIssues(request);
|
|
998
|
+
return {
|
|
999
|
+
jql: request.jql,
|
|
1000
|
+
startAt: result.startAt,
|
|
1001
|
+
maxResults: result.maxResults,
|
|
1002
|
+
total: result.total,
|
|
1003
|
+
issues: [...result.issues]
|
|
1004
|
+
};
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
rethrowJiraOperationError(error, "Failed to search Jira issues.");
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
renderText: (output) => renderSearchIssues(output)
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
createGetIssueTool() {
|
|
1013
|
+
return defineTool({
|
|
1014
|
+
name: "get_issue",
|
|
1015
|
+
title: "Get Jira issue",
|
|
1016
|
+
description: "Fetch a Jira issue with normalized fields, description text, and comments.",
|
|
1017
|
+
inputSchema: getIssueInputShape,
|
|
1018
|
+
outputSchema: getIssueOutputShape,
|
|
1019
|
+
handler: async (input, context) => {
|
|
1020
|
+
await context.log("info", `Fetching Jira issue ${input.issueKey}`);
|
|
1021
|
+
try {
|
|
1022
|
+
const issue = await this.client.getIssue(input.issueKey, input.fields ?? DEFAULT_ISSUE_FIELDS);
|
|
1023
|
+
return {
|
|
1024
|
+
issue
|
|
1025
|
+
};
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
rethrowJiraOperationError(error, `Failed to fetch Jira issue '${input.issueKey}'.`);
|
|
1028
|
+
}
|
|
1029
|
+
},
|
|
1030
|
+
renderText: (output) => renderIssueDetail(output)
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
createTransitionIssueTool() {
|
|
1034
|
+
return defineTool({
|
|
1035
|
+
name: "transition_issue",
|
|
1036
|
+
title: "Transition Jira issue",
|
|
1037
|
+
description: "Safely resolve a Jira transition by name or id, apply it, and return the updated issue.",
|
|
1038
|
+
inputSchema: transitionIssueInputShape,
|
|
1039
|
+
outputSchema: transitionIssueOutputShape,
|
|
1040
|
+
handler: async (input, context) => {
|
|
1041
|
+
ensureTransitionInput(input);
|
|
1042
|
+
await context.log("info", `Resolving transitions for Jira issue ${input.issueKey}`);
|
|
1043
|
+
try {
|
|
1044
|
+
const transitions = await this.client.getTransitions(input.issueKey);
|
|
1045
|
+
const transition = resolveTransition(transitions, input, input.issueKey);
|
|
1046
|
+
await context.log("info", `Applying Jira transition '${transition.name}' (${transition.id})`);
|
|
1047
|
+
await this.client.transitionIssue(input.issueKey, transition.id, input.comment);
|
|
1048
|
+
const issue = await this.client.getIssue(input.issueKey);
|
|
1049
|
+
return {
|
|
1050
|
+
issueKey: input.issueKey,
|
|
1051
|
+
transition,
|
|
1052
|
+
commentAdded: input.comment !== void 0,
|
|
1053
|
+
availableTransitions: [...transitions],
|
|
1054
|
+
issue
|
|
1055
|
+
};
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
rethrowJiraOperationError(error, `Failed to transition Jira issue '${input.issueKey}'.`);
|
|
1058
|
+
}
|
|
1059
|
+
},
|
|
1060
|
+
renderText: (output) => renderTransitionResult(output)
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
registerProjectResource() {
|
|
1064
|
+
this.registerTemplateResource(
|
|
1065
|
+
"project",
|
|
1066
|
+
"jira://projects/{projectKey}",
|
|
1067
|
+
{
|
|
1068
|
+
title: "Jira project",
|
|
1069
|
+
description: "Return normalized Jira project metadata as JSON.",
|
|
1070
|
+
mimeType: "application/json"
|
|
1071
|
+
},
|
|
1072
|
+
async (uri, variables) => {
|
|
1073
|
+
const parsed = projectResourceParamsSchema.safeParse({
|
|
1074
|
+
projectKey: Array.isArray(variables.projectKey) ? variables.projectKey[0] : variables.projectKey
|
|
1075
|
+
});
|
|
1076
|
+
if (!parsed.success) {
|
|
1077
|
+
throw new ValidationError("Invalid Jira project resource URI.", parsed.error.flatten());
|
|
1078
|
+
}
|
|
1079
|
+
return this.readProjectResource(parsed.data.projectKey, uri.toString());
|
|
1080
|
+
}
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
registerIncidentTriagePrompt() {
|
|
1084
|
+
this.registerPrompt(
|
|
1085
|
+
"incident_triage",
|
|
1086
|
+
{
|
|
1087
|
+
title: "Incident triage",
|
|
1088
|
+
description: "Generate an incident-triage prompt from Jira issue context.",
|
|
1089
|
+
argsSchema: incidentTriagePromptArgsShape
|
|
1090
|
+
},
|
|
1091
|
+
(args) => this.buildIncidentTriagePrompt(args)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
function createServer(options = {}) {
|
|
1096
|
+
return new JiraServer(options);
|
|
1097
|
+
}
|
|
1098
|
+
var runtimeRegistration = {
|
|
1099
|
+
createServer,
|
|
1100
|
+
serverCard
|
|
1101
|
+
};
|
|
1102
|
+
async function main(argv = process.argv.slice(2)) {
|
|
1103
|
+
const runtimeOptions = parseRuntimeOptions(argv);
|
|
1104
|
+
await runToolkitServer(runtimeRegistration, runtimeOptions);
|
|
1105
|
+
}
|
|
1106
|
+
var index_default = runtimeRegistration;
|
|
1107
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1108
|
+
void main().catch((error) => {
|
|
1109
|
+
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1110
|
+
process.stderr.write(`${message}
|
|
1111
|
+
`);
|
|
1112
|
+
process.exitCode = 1;
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
export {
|
|
1116
|
+
JiraRestClient,
|
|
1117
|
+
JiraServer,
|
|
1118
|
+
createServer,
|
|
1119
|
+
index_default as default,
|
|
1120
|
+
loadJiraEnvironment,
|
|
1121
|
+
main,
|
|
1122
|
+
metadata,
|
|
1123
|
+
serverCard
|
|
1124
|
+
};
|