@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/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
+ };