@rui.branco/jira-mcp 1.6.12 → 1.6.13
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/index.js +1622 -265
- package/package.json +3 -2
package/index.js
CHANGED
|
@@ -160,6 +160,30 @@ async function fetchJira(endpoint, options = {}, instance = defaultInstance) {
|
|
|
160
160
|
return text ? JSON.parse(text) : {};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
async function fetchJiraAgile(endpoint, options = {}, instance = defaultInstance) {
|
|
164
|
+
const { method = "GET", body } = options;
|
|
165
|
+
const headers = {
|
|
166
|
+
Authorization: `Basic ${instance.auth}`,
|
|
167
|
+
Accept: "application/json",
|
|
168
|
+
};
|
|
169
|
+
if (body) {
|
|
170
|
+
headers["Content-Type"] = "application/json";
|
|
171
|
+
}
|
|
172
|
+
const response = await fetch(`${instance.baseUrl}/rest/agile/1.0${endpoint}`, {
|
|
173
|
+
method,
|
|
174
|
+
headers,
|
|
175
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
176
|
+
});
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
const errorBody = await response.text().catch(() => "");
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Jira Agile API error: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const text = await response.text();
|
|
184
|
+
return text ? JSON.parse(text) : {};
|
|
185
|
+
}
|
|
186
|
+
|
|
163
187
|
async function downloadAttachment(url, filename, issueKey, instance) {
|
|
164
188
|
const issueDir = path.join(attachmentDir, issueKey);
|
|
165
189
|
if (!fs.existsSync(issueDir)) {
|
|
@@ -195,14 +219,22 @@ function extractText(content, urls = []) {
|
|
|
195
219
|
if (content.content) {
|
|
196
220
|
for (const node of content.content) {
|
|
197
221
|
if (node.type === "text") {
|
|
198
|
-
|
|
222
|
+
const nodeText = node.text || "";
|
|
199
223
|
// Check for link marks
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
224
|
+
const linkMark = node.marks?.find(
|
|
225
|
+
(m) => m.type === "link" && m.attrs?.href,
|
|
226
|
+
);
|
|
227
|
+
if (linkMark) {
|
|
228
|
+
const href = linkMark.attrs.href;
|
|
229
|
+
urls.push(href);
|
|
230
|
+
// Render as markdown link if the display text differs from the URL
|
|
231
|
+
if (nodeText && nodeText !== href) {
|
|
232
|
+
text += `[${nodeText}](${href})`;
|
|
233
|
+
} else {
|
|
234
|
+
text += href;
|
|
205
235
|
}
|
|
236
|
+
} else {
|
|
237
|
+
text += nodeText;
|
|
206
238
|
}
|
|
207
239
|
} else if (node.type === "paragraph") {
|
|
208
240
|
const result = extractText(node, urls);
|
|
@@ -284,11 +316,11 @@ async function searchUser(query, instance = defaultInstance) {
|
|
|
284
316
|
}
|
|
285
317
|
|
|
286
318
|
// Parse text with @mentions and build ADF content
|
|
287
|
-
// Parse inline formatting: **bold**, *italic*, @mentions
|
|
319
|
+
// Parse inline formatting: **bold**, *italic*, `code`, [links](url), @mentions
|
|
288
320
|
async function parseInlineFormatting(text, instance = defaultInstance) {
|
|
289
321
|
const nodes = [];
|
|
290
|
-
//
|
|
291
|
-
const regex = /(
|
|
322
|
+
// Links, bold, italic, inline code, mentions — links must come first to avoid ** inside link text being parsed as bold
|
|
323
|
+
const regex = /(\[([^\]]+)\]\(([^)]+)\)|`(.+?)`|\*\*(.+?)\*\*|\*(.+?)\*|~~(.+?)~~|@([A-Z][a-zA-Zà-ÿ]*(?:\s[A-Z][a-zA-Zà-ÿ]*)*))/g;
|
|
292
324
|
|
|
293
325
|
let lastIndex = 0;
|
|
294
326
|
let match;
|
|
@@ -298,23 +330,45 @@ async function parseInlineFormatting(text, instance = defaultInstance) {
|
|
|
298
330
|
nodes.push({ type: "text", text: text.substring(lastIndex, match.index) });
|
|
299
331
|
}
|
|
300
332
|
|
|
301
|
-
if (match[2] !== undefined) {
|
|
333
|
+
if (match[2] !== undefined && match[3] !== undefined) {
|
|
334
|
+
// [link text](url)
|
|
335
|
+
nodes.push({
|
|
336
|
+
type: "text",
|
|
337
|
+
text: match[2],
|
|
338
|
+
marks: [{ type: "link", attrs: { href: match[3] } }],
|
|
339
|
+
});
|
|
340
|
+
} else if (match[4] !== undefined) {
|
|
302
341
|
// `inline code`
|
|
303
|
-
nodes.push({ type: "text", text: match[
|
|
304
|
-
} else if (match[
|
|
342
|
+
nodes.push({ type: "text", text: match[4], marks: [{ type: "code" }] });
|
|
343
|
+
} else if (match[5] !== undefined) {
|
|
305
344
|
// **bold**
|
|
306
|
-
nodes.push({ type: "text", text: match[
|
|
307
|
-
} else if (match[
|
|
345
|
+
nodes.push({ type: "text", text: match[5], marks: [{ type: "strong" }] });
|
|
346
|
+
} else if (match[6] !== undefined) {
|
|
308
347
|
// *italic*
|
|
309
|
-
nodes.push({ type: "text", text: match[
|
|
310
|
-
} else if (match[
|
|
311
|
-
//
|
|
312
|
-
|
|
348
|
+
nodes.push({ type: "text", text: match[6], marks: [{ type: "em" }] });
|
|
349
|
+
} else if (match[7] !== undefined) {
|
|
350
|
+
// ~~strikethrough~~
|
|
351
|
+
nodes.push({ type: "text", text: match[7], marks: [{ type: "strike" }] });
|
|
352
|
+
} else if (match[8] !== undefined) {
|
|
353
|
+
// @Mention — regex may greedily capture extra capitalized words beyond the actual name.
|
|
354
|
+
// After resolving, only consume the portion matching the display name.
|
|
355
|
+
const captured = match[8].trim();
|
|
356
|
+
const user = await searchUser(captured, instance);
|
|
313
357
|
if (user) {
|
|
314
358
|
nodes.push({
|
|
315
359
|
type: "mention",
|
|
316
360
|
attrs: { id: user.accountId, text: `@${user.displayName}` },
|
|
317
361
|
});
|
|
362
|
+
// Only consume "@" + display name length from the captured text.
|
|
363
|
+
// If the captured text starts with the display name (case-insensitive),
|
|
364
|
+
// use the display name length; otherwise fall back to full match.
|
|
365
|
+
const dn = user.displayName;
|
|
366
|
+
const consumeLen = captured.toLowerCase().startsWith(dn.toLowerCase())
|
|
367
|
+
? dn.length
|
|
368
|
+
: captured.length;
|
|
369
|
+
lastIndex = match.index + 1 + consumeLen;
|
|
370
|
+
regex.lastIndex = lastIndex;
|
|
371
|
+
continue;
|
|
318
372
|
} else {
|
|
319
373
|
nodes.push({ type: "text", text: match[0] });
|
|
320
374
|
}
|
|
@@ -334,37 +388,167 @@ async function parseInlineFormatting(text, instance = defaultInstance) {
|
|
|
334
388
|
async function buildCommentADF(text, instance = defaultInstance) {
|
|
335
389
|
// Sanitize: replace em dashes and en dashes with hyphen
|
|
336
390
|
text = text.replace(/[—–]/g, "-");
|
|
337
|
-
|
|
338
|
-
const blocks = text.split(/\n\n+/);
|
|
391
|
+
|
|
339
392
|
const content = [];
|
|
393
|
+
const lines = text.split("\n");
|
|
394
|
+
let i = 0;
|
|
395
|
+
|
|
396
|
+
while (i < lines.length) {
|
|
397
|
+
const line = lines[i];
|
|
398
|
+
const trimmed = line.trim();
|
|
340
399
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
400
|
+
// Skip empty lines
|
|
401
|
+
if (!trimmed) {
|
|
402
|
+
i++;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// --- Code block (triple backticks) ---
|
|
407
|
+
if (trimmed.startsWith("```")) {
|
|
408
|
+
const lang = trimmed.substring(3).trim() || null;
|
|
409
|
+
const codeLines = [];
|
|
410
|
+
i++;
|
|
411
|
+
while (i < lines.length && !lines[i].trim().startsWith("```")) {
|
|
412
|
+
codeLines.push(lines[i]);
|
|
413
|
+
i++;
|
|
414
|
+
}
|
|
415
|
+
if (i < lines.length) i++; // skip closing ```
|
|
416
|
+
const codeBlock = {
|
|
417
|
+
type: "codeBlock",
|
|
418
|
+
content: [{ type: "text", text: codeLines.join("\n") }],
|
|
419
|
+
};
|
|
420
|
+
if (lang) codeBlock.attrs = { language: lang };
|
|
421
|
+
content.push(codeBlock);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// --- Horizontal rule ---
|
|
426
|
+
if (/^-{3,}$/.test(trimmed) || /^\*{3,}$/.test(trimmed) || /^_{3,}$/.test(trimmed)) {
|
|
427
|
+
content.push({ type: "rule" });
|
|
428
|
+
i++;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// --- Headings (# to ######) ---
|
|
433
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
|
|
434
|
+
if (headingMatch) {
|
|
435
|
+
const level = headingMatch[1].length;
|
|
436
|
+
const headingText = headingMatch[2];
|
|
437
|
+
const inlineContent = await parseInlineFormatting(headingText, instance);
|
|
438
|
+
content.push({
|
|
439
|
+
type: "heading",
|
|
440
|
+
attrs: { level },
|
|
441
|
+
content: inlineContent,
|
|
442
|
+
});
|
|
443
|
+
i++;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// --- Blockquote (> lines) ---
|
|
448
|
+
if (trimmed.startsWith("> ")) {
|
|
449
|
+
const quoteLines = [];
|
|
450
|
+
while (i < lines.length && lines[i].trim().startsWith("> ")) {
|
|
451
|
+
quoteLines.push(lines[i].trim().substring(2));
|
|
452
|
+
i++;
|
|
453
|
+
}
|
|
454
|
+
const quoteContent = await parseInlineFormatting(quoteLines.join("\n"), instance);
|
|
455
|
+
content.push({
|
|
456
|
+
type: "blockquote",
|
|
457
|
+
content: [{ type: "paragraph", content: quoteContent }],
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
344
461
|
|
|
345
|
-
|
|
346
|
-
|
|
462
|
+
// --- Table (pipe-separated) ---
|
|
463
|
+
if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
|
|
464
|
+
const tableRows = [];
|
|
465
|
+
while (i < lines.length && lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|")) {
|
|
466
|
+
const row = lines[i].trim();
|
|
467
|
+
// Skip separator rows (|---|---|)
|
|
468
|
+
if (/^\|[\s\-:|]+\|$/.test(row)) {
|
|
469
|
+
i++;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const cells = row
|
|
473
|
+
.substring(1, row.length - 1)
|
|
474
|
+
.split("|")
|
|
475
|
+
.map((c) => c.trim());
|
|
476
|
+
tableRows.push(cells);
|
|
477
|
+
i++;
|
|
478
|
+
}
|
|
479
|
+
if (tableRows.length > 0) {
|
|
480
|
+
const isHeader = tableRows.length > 1;
|
|
481
|
+
const adfRows = [];
|
|
482
|
+
for (let r = 0; r < tableRows.length; r++) {
|
|
483
|
+
const cellType = r === 0 && isHeader ? "tableHeader" : "tableCell";
|
|
484
|
+
const adfCells = [];
|
|
485
|
+
for (const cellText of tableRows[r]) {
|
|
486
|
+
const inlineContent = await parseInlineFormatting(cellText, instance);
|
|
487
|
+
adfCells.push({
|
|
488
|
+
type: cellType,
|
|
489
|
+
content: [{ type: "paragraph", content: inlineContent }],
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
adfRows.push({ type: "tableRow", content: adfCells });
|
|
493
|
+
}
|
|
494
|
+
content.push({ type: "table", content: adfRows });
|
|
495
|
+
}
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
347
498
|
|
|
348
|
-
|
|
349
|
-
|
|
499
|
+
// --- Bullet list (- items) ---
|
|
500
|
+
if (trimmed.startsWith("- ")) {
|
|
350
501
|
const listItems = [];
|
|
351
|
-
|
|
352
|
-
const itemText =
|
|
502
|
+
while (i < lines.length && lines[i].trim().startsWith("- ")) {
|
|
503
|
+
const itemText = lines[i].trim().substring(2);
|
|
353
504
|
const inlineContent = await parseInlineFormatting(itemText, instance);
|
|
354
505
|
listItems.push({
|
|
355
506
|
type: "listItem",
|
|
356
507
|
content: [{ type: "paragraph", content: inlineContent }],
|
|
357
508
|
});
|
|
509
|
+
i++;
|
|
358
510
|
}
|
|
359
511
|
content.push({ type: "bulletList", content: listItems });
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// --- Ordered list (1. items) ---
|
|
516
|
+
if (/^\d+\.\s/.test(trimmed)) {
|
|
517
|
+
const listItems = [];
|
|
518
|
+
while (i < lines.length && /^\d+\.\s/.test(lines[i].trim())) {
|
|
519
|
+
const itemText = lines[i].trim().replace(/^\d+\.\s/, "");
|
|
520
|
+
const inlineContent = await parseInlineFormatting(itemText, instance);
|
|
521
|
+
listItems.push({
|
|
522
|
+
type: "listItem",
|
|
523
|
+
content: [{ type: "paragraph", content: inlineContent }],
|
|
524
|
+
});
|
|
525
|
+
i++;
|
|
367
526
|
}
|
|
527
|
+
content.push({ type: "orderedList", content: listItems });
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// --- Regular paragraph (collect consecutive non-special lines) ---
|
|
532
|
+
const paragraphContent = [];
|
|
533
|
+
while (
|
|
534
|
+
i < lines.length &&
|
|
535
|
+
lines[i].trim() &&
|
|
536
|
+
!lines[i].trim().startsWith("```") &&
|
|
537
|
+
!lines[i].trim().startsWith("# ") &&
|
|
538
|
+
!lines[i].trim().startsWith("## ") &&
|
|
539
|
+
!lines[i].trim().startsWith("### ") &&
|
|
540
|
+
!lines[i].trim().startsWith("> ") &&
|
|
541
|
+
!lines[i].trim().startsWith("- ") &&
|
|
542
|
+
!/^\d+\.\s/.test(lines[i].trim()) &&
|
|
543
|
+
!/^-{3,}$/.test(lines[i].trim()) &&
|
|
544
|
+
!(lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|"))
|
|
545
|
+
) {
|
|
546
|
+
if (paragraphContent.length > 0) paragraphContent.push({ type: "hardBreak" });
|
|
547
|
+
const inlineNodes = await parseInlineFormatting(lines[i].trim(), instance);
|
|
548
|
+
paragraphContent.push(...inlineNodes);
|
|
549
|
+
i++;
|
|
550
|
+
}
|
|
551
|
+
if (paragraphContent.length > 0) {
|
|
368
552
|
content.push({ type: "paragraph", content: paragraphContent });
|
|
369
553
|
}
|
|
370
554
|
}
|
|
@@ -1585,124 +1769,672 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1585
1769
|
required: [],
|
|
1586
1770
|
},
|
|
1587
1771
|
},
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1772
|
+
{
|
|
1773
|
+
name: "jira_create_ticket",
|
|
1774
|
+
description:
|
|
1775
|
+
"Create a new Jira issue (story, task, bug, epic, etc.). Returns the new issue key and URL. Use jira_search_users to get account IDs for assignee. NEVER use em dashes or en dashes in text fields.",
|
|
1776
|
+
inputSchema: {
|
|
1777
|
+
type: "object",
|
|
1778
|
+
properties: {
|
|
1779
|
+
projectKey: {
|
|
1780
|
+
type: "string",
|
|
1781
|
+
description: "The project key (e.g., MODS, ENG)",
|
|
1782
|
+
},
|
|
1783
|
+
issueType: {
|
|
1784
|
+
type: "string",
|
|
1785
|
+
description: "Issue type name (e.g., Story, Task, Bug, Epic, Sub-task)",
|
|
1786
|
+
},
|
|
1787
|
+
summary: {
|
|
1788
|
+
type: "string",
|
|
1789
|
+
description: "The issue title/summary",
|
|
1790
|
+
},
|
|
1791
|
+
description: {
|
|
1792
|
+
type: "string",
|
|
1793
|
+
description: "The issue description text. Supports @mentions via @DisplayName.",
|
|
1794
|
+
},
|
|
1795
|
+
assignee: {
|
|
1796
|
+
type: "string",
|
|
1797
|
+
description: "Assignee account ID. Use jira_search_users or jira_get_myself to get this.",
|
|
1798
|
+
},
|
|
1799
|
+
priority: {
|
|
1800
|
+
type: "string",
|
|
1801
|
+
description: "Priority name (e.g., Highest, High, Medium, Low, Lowest)",
|
|
1802
|
+
},
|
|
1803
|
+
labels: {
|
|
1804
|
+
type: "array",
|
|
1805
|
+
items: { type: "string" },
|
|
1806
|
+
description: "Labels to set on the ticket",
|
|
1807
|
+
},
|
|
1808
|
+
components: {
|
|
1809
|
+
type: "array",
|
|
1810
|
+
items: { type: "string" },
|
|
1811
|
+
description: "Component names to set on the ticket",
|
|
1812
|
+
},
|
|
1813
|
+
fixVersions: {
|
|
1814
|
+
type: "array",
|
|
1815
|
+
items: { type: "string" },
|
|
1816
|
+
description: "Fix version names to set on the ticket",
|
|
1817
|
+
},
|
|
1818
|
+
storyPoints: {
|
|
1819
|
+
type: "number",
|
|
1820
|
+
description: "Story points value",
|
|
1821
|
+
},
|
|
1822
|
+
parentKey: {
|
|
1823
|
+
type: "string",
|
|
1824
|
+
description: "Parent issue key for creating issues within an epic (e.g., MODS-100). For subtasks, use jira_create_subtask instead.",
|
|
1825
|
+
},
|
|
1826
|
+
sprintId: {
|
|
1827
|
+
type: "number",
|
|
1828
|
+
description: "Sprint ID to add the ticket to. Use jira_get_sprints to find sprint IDs.",
|
|
1829
|
+
},
|
|
1830
|
+
instance: {
|
|
1831
|
+
type: "string",
|
|
1832
|
+
description: "Instance name override. Auto-detected from project key if omitted.",
|
|
1833
|
+
},
|
|
1604
1834
|
},
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
{
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1835
|
+
required: ["projectKey", "issueType", "summary"],
|
|
1836
|
+
},
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
name: "jira_create_subtask",
|
|
1840
|
+
description:
|
|
1841
|
+
"Create a subtask under a parent Jira ticket. The subtask is created in the same project as the parent. NEVER use em dashes or en dashes in text fields.",
|
|
1842
|
+
inputSchema: {
|
|
1843
|
+
type: "object",
|
|
1844
|
+
properties: {
|
|
1845
|
+
parentKey: {
|
|
1846
|
+
type: "string",
|
|
1847
|
+
description: "The parent issue key (e.g., MODS-123)",
|
|
1848
|
+
},
|
|
1849
|
+
summary: {
|
|
1850
|
+
type: "string",
|
|
1851
|
+
description: "The subtask title/summary",
|
|
1852
|
+
},
|
|
1853
|
+
description: {
|
|
1854
|
+
type: "string",
|
|
1855
|
+
description: "The subtask description text. Supports @mentions via @DisplayName.",
|
|
1856
|
+
},
|
|
1857
|
+
assignee: {
|
|
1858
|
+
type: "string",
|
|
1859
|
+
description: "Assignee account ID. Use jira_search_users or jira_get_myself to get this.",
|
|
1860
|
+
},
|
|
1861
|
+
priority: {
|
|
1862
|
+
type: "string",
|
|
1863
|
+
description: "Priority name (e.g., Highest, High, Medium, Low, Lowest)",
|
|
1864
|
+
},
|
|
1865
|
+
labels: {
|
|
1866
|
+
type: "array",
|
|
1867
|
+
items: { type: "string" },
|
|
1868
|
+
description: "Labels to set on the subtask",
|
|
1869
|
+
},
|
|
1870
|
+
instance: {
|
|
1871
|
+
type: "string",
|
|
1872
|
+
description: "Instance name override. Auto-detected from parent issue key prefix if omitted.",
|
|
1621
1873
|
},
|
|
1622
|
-
],
|
|
1623
|
-
};
|
|
1624
|
-
}
|
|
1625
|
-
const lines = users.map(
|
|
1626
|
-
(u) =>
|
|
1627
|
-
`- **${u.displayName}** (accountId: ${u.accountId}${u.emailAddress ? `, email: ${u.emailAddress}` : ""})`,
|
|
1628
|
-
);
|
|
1629
|
-
return {
|
|
1630
|
-
content: [
|
|
1631
|
-
{
|
|
1632
|
-
type: "text",
|
|
1633
|
-
text: `Found ${users.length} user(s) for "${args.query}":\n\n${lines.join("\n")}`,
|
|
1634
1874
|
},
|
|
1635
|
-
|
|
1636
|
-
};
|
|
1637
|
-
} else if (name === "jira_get_ticket") {
|
|
1638
|
-
if (!args.issueKey) {
|
|
1639
|
-
return { content: [{ type: "text", text: "Error: Missing required parameter 'issueKey'. You passed 'ticketId' which is not a valid parameter. Use 'issueKey' instead (e.g. issueKey: \"MODS-12115\")." }] };
|
|
1640
|
-
}
|
|
1641
|
-
const downloadImages = args.downloadImages !== false;
|
|
1642
|
-
const fetchFigma = args.fetchFigma !== false;
|
|
1643
|
-
const inst = args.instance ? getInstanceByName(args.instance) : null;
|
|
1644
|
-
const result = await getTicket(args.issueKey, downloadImages, fetchFigma, inst);
|
|
1645
|
-
|
|
1646
|
-
const content = [{ type: "text", text: result.text }];
|
|
1647
|
-
|
|
1648
|
-
// Add Jira images
|
|
1649
|
-
for (const imagePath of result.jiraImages) {
|
|
1650
|
-
try {
|
|
1651
|
-
const imageData = fs.readFileSync(imagePath);
|
|
1652
|
-
const ext = path.extname(imagePath).toLowerCase();
|
|
1653
|
-
const mimeType =
|
|
1654
|
-
ext === ".png"
|
|
1655
|
-
? "image/png"
|
|
1656
|
-
: ext === ".gif"
|
|
1657
|
-
? "image/gif"
|
|
1658
|
-
: ext === ".webp"
|
|
1659
|
-
? "image/webp"
|
|
1660
|
-
: "image/jpeg";
|
|
1661
|
-
content.push({
|
|
1662
|
-
type: "image",
|
|
1663
|
-
data: imageData.toString("base64"),
|
|
1664
|
-
mimeType: mimeType,
|
|
1665
|
-
});
|
|
1666
|
-
} catch (e) {
|
|
1667
|
-
/* skip */
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
// Add Figma images (now supports multiple images per design)
|
|
1672
|
-
for (const design of result.figmaDesigns) {
|
|
1673
|
-
if (design.images && design.images.length > 0) {
|
|
1674
|
-
for (const img of design.images) {
|
|
1675
|
-
if (img.buffer) {
|
|
1676
|
-
content.push({
|
|
1677
|
-
type: "image",
|
|
1678
|
-
data: img.buffer.toString("base64"),
|
|
1679
|
-
mimeType: "image/png",
|
|
1680
|
-
});
|
|
1681
|
-
}
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
return { content };
|
|
1687
|
-
} else if (name === "jira_search") {
|
|
1688
|
-
const inst = getInstanceByName(args.instance);
|
|
1689
|
-
const result = await searchTickets(args.jql, args.maxResults || 10, args.fields || null, inst);
|
|
1690
|
-
return { content: [{ type: "text", text: result }] };
|
|
1691
|
-
} else if (name === "jira_add_comment") {
|
|
1692
|
-
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
1693
|
-
// Build ADF content with mention support
|
|
1694
|
-
const adfContent = await buildCommentADF(args.comment, inst);
|
|
1695
|
-
const body = {
|
|
1696
|
-
body: {
|
|
1697
|
-
version: 1,
|
|
1698
|
-
type: "doc",
|
|
1699
|
-
content: adfContent,
|
|
1875
|
+
required: ["parentKey", "summary"],
|
|
1700
1876
|
},
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1877
|
+
},
|
|
1878
|
+
{
|
|
1879
|
+
name: "jira_link_tickets",
|
|
1880
|
+
description:
|
|
1881
|
+
"Link two Jira tickets together with a relationship type. Use jira_get_link_types to discover available link type names. Common types: 'Blocks' (blocks/is blocked by), 'Relates' (relates to), 'Duplicate' (duplicates/is duplicated by), 'Cloners' (clones/is cloned by).",
|
|
1882
|
+
inputSchema: {
|
|
1883
|
+
type: "object",
|
|
1884
|
+
properties: {
|
|
1885
|
+
inwardIssueKey: {
|
|
1886
|
+
type: "string",
|
|
1887
|
+
description: "The inward issue key (e.g., MODS-123). Receives the inward description (e.g., 'is blocked by').",
|
|
1888
|
+
},
|
|
1889
|
+
outwardIssueKey: {
|
|
1890
|
+
type: "string",
|
|
1891
|
+
description: "The outward issue key (e.g., MODS-456). Receives the outward description (e.g., 'blocks').",
|
|
1892
|
+
},
|
|
1893
|
+
linkType: {
|
|
1894
|
+
type: "string",
|
|
1895
|
+
description: "The link type name (e.g., 'Blocks', 'Relates', 'Duplicate'). Use jira_get_link_types to see available types.",
|
|
1896
|
+
},
|
|
1897
|
+
comment: {
|
|
1898
|
+
type: "string",
|
|
1899
|
+
description: "Optional comment to add when creating the link.",
|
|
1900
|
+
},
|
|
1901
|
+
instance: {
|
|
1902
|
+
type: "string",
|
|
1903
|
+
description: "Instance name override. Auto-detected from inward issue key prefix if omitted.",
|
|
1904
|
+
},
|
|
1905
|
+
},
|
|
1906
|
+
required: ["inwardIssueKey", "outwardIssueKey", "linkType"],
|
|
1907
|
+
},
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
name: "jira_delete_ticket",
|
|
1911
|
+
description:
|
|
1912
|
+
"Delete a Jira ticket. This action is IRREVERSIBLE. If the ticket has subtasks, set deleteSubtasks to true or the deletion will fail.",
|
|
1913
|
+
inputSchema: {
|
|
1914
|
+
type: "object",
|
|
1915
|
+
properties: {
|
|
1916
|
+
issueKey: {
|
|
1917
|
+
type: "string",
|
|
1918
|
+
description: "The Jira issue key to delete (e.g., MODS-123)",
|
|
1919
|
+
},
|
|
1920
|
+
deleteSubtasks: {
|
|
1921
|
+
type: "boolean",
|
|
1922
|
+
description: "If true, also deletes all subtasks. If false (default), deletion fails when subtasks exist.",
|
|
1923
|
+
},
|
|
1924
|
+
instance: {
|
|
1925
|
+
type: "string",
|
|
1926
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1927
|
+
},
|
|
1928
|
+
},
|
|
1929
|
+
required: ["issueKey"],
|
|
1930
|
+
},
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
name: "jira_add_attachment",
|
|
1934
|
+
description:
|
|
1935
|
+
"Upload a file attachment to a Jira ticket. Provide either a local file path or base64-encoded file content.",
|
|
1936
|
+
inputSchema: {
|
|
1937
|
+
type: "object",
|
|
1938
|
+
properties: {
|
|
1939
|
+
issueKey: {
|
|
1940
|
+
type: "string",
|
|
1941
|
+
description: "The Jira issue key (e.g., MODS-123)",
|
|
1942
|
+
},
|
|
1943
|
+
filePath: {
|
|
1944
|
+
type: "string",
|
|
1945
|
+
description: "Absolute path to the local file to upload. Use this OR fileContent, not both.",
|
|
1946
|
+
},
|
|
1947
|
+
fileContent: {
|
|
1948
|
+
type: "string",
|
|
1949
|
+
description: "Base64-encoded file content. Must also provide fileName when using this.",
|
|
1950
|
+
},
|
|
1951
|
+
fileName: {
|
|
1952
|
+
type: "string",
|
|
1953
|
+
description: "File name (required when using fileContent, optional with filePath to override the original name).",
|
|
1954
|
+
},
|
|
1955
|
+
instance: {
|
|
1956
|
+
type: "string",
|
|
1957
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1958
|
+
},
|
|
1959
|
+
},
|
|
1960
|
+
required: ["issueKey"],
|
|
1961
|
+
},
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
name: "jira_get_link_types",
|
|
1965
|
+
description:
|
|
1966
|
+
"List all available issue link types in the Jira instance. Use this to discover valid link type names before using jira_link_tickets.",
|
|
1967
|
+
inputSchema: {
|
|
1968
|
+
type: "object",
|
|
1969
|
+
properties: {
|
|
1970
|
+
instance: {
|
|
1971
|
+
type: "string",
|
|
1972
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
1973
|
+
},
|
|
1974
|
+
},
|
|
1975
|
+
required: [],
|
|
1976
|
+
},
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
name: "jira_get_transitions",
|
|
1980
|
+
description:
|
|
1981
|
+
"List available status transitions for a Jira ticket. Use this to discover valid transition names/IDs before using jira_transition.",
|
|
1982
|
+
inputSchema: {
|
|
1983
|
+
type: "object",
|
|
1984
|
+
properties: {
|
|
1985
|
+
issueKey: {
|
|
1986
|
+
type: "string",
|
|
1987
|
+
description: "The Jira issue key (e.g., MODS-123)",
|
|
1988
|
+
},
|
|
1989
|
+
instance: {
|
|
1990
|
+
type: "string",
|
|
1991
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1992
|
+
},
|
|
1993
|
+
},
|
|
1994
|
+
required: ["issueKey"],
|
|
1995
|
+
},
|
|
1996
|
+
},
|
|
1997
|
+
{
|
|
1998
|
+
name: "jira_unlink_tickets",
|
|
1999
|
+
description:
|
|
2000
|
+
"Remove a link between two Jira tickets. Fetches the issue to find the link ID automatically.",
|
|
2001
|
+
inputSchema: {
|
|
2002
|
+
type: "object",
|
|
2003
|
+
properties: {
|
|
2004
|
+
issueKey: {
|
|
2005
|
+
type: "string",
|
|
2006
|
+
description: "One of the linked issue keys (e.g., MODS-123). The link will be found from this issue.",
|
|
2007
|
+
},
|
|
2008
|
+
linkedIssueKey: {
|
|
2009
|
+
type: "string",
|
|
2010
|
+
description: "The other linked issue key (e.g., MODS-456).",
|
|
2011
|
+
},
|
|
2012
|
+
instance: {
|
|
2013
|
+
type: "string",
|
|
2014
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
2015
|
+
},
|
|
2016
|
+
},
|
|
2017
|
+
required: ["issueKey", "linkedIssueKey"],
|
|
2018
|
+
},
|
|
2019
|
+
},
|
|
2020
|
+
{
|
|
2021
|
+
name: "jira_remove_attachment",
|
|
2022
|
+
description:
|
|
2023
|
+
"Remove an attachment from a Jira ticket by attachment ID or filename. If using filename, fetches the ticket to find the attachment ID.",
|
|
2024
|
+
inputSchema: {
|
|
2025
|
+
type: "object",
|
|
2026
|
+
properties: {
|
|
2027
|
+
issueKey: {
|
|
2028
|
+
type: "string",
|
|
2029
|
+
description: "The Jira issue key (e.g., MODS-123). Required when using fileName.",
|
|
2030
|
+
},
|
|
2031
|
+
attachmentId: {
|
|
2032
|
+
type: "string",
|
|
2033
|
+
description: "The attachment ID to delete. Use this OR fileName.",
|
|
2034
|
+
},
|
|
2035
|
+
fileName: {
|
|
2036
|
+
type: "string",
|
|
2037
|
+
description: "The attachment filename to delete. Use this OR attachmentId.",
|
|
2038
|
+
},
|
|
2039
|
+
instance: {
|
|
2040
|
+
type: "string",
|
|
2041
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
2042
|
+
},
|
|
2043
|
+
},
|
|
2044
|
+
required: [],
|
|
2045
|
+
},
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
name: "jira_add_watcher",
|
|
2049
|
+
description:
|
|
2050
|
+
"Add a watcher to a Jira ticket. The watcher will receive notifications for changes on the ticket.",
|
|
2051
|
+
inputSchema: {
|
|
2052
|
+
type: "object",
|
|
2053
|
+
properties: {
|
|
2054
|
+
issueKey: {
|
|
2055
|
+
type: "string",
|
|
2056
|
+
description: "The Jira issue key (e.g., MODS-123)",
|
|
2057
|
+
},
|
|
2058
|
+
accountId: {
|
|
2059
|
+
type: "string",
|
|
2060
|
+
description: "The account ID of the user to add as watcher. Use jira_search_users to find this.",
|
|
2061
|
+
},
|
|
2062
|
+
instance: {
|
|
2063
|
+
type: "string",
|
|
2064
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
2065
|
+
},
|
|
2066
|
+
},
|
|
2067
|
+
required: ["issueKey", "accountId"],
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
name: "jira_remove_watcher",
|
|
2072
|
+
description:
|
|
2073
|
+
"Remove a watcher from a Jira ticket.",
|
|
2074
|
+
inputSchema: {
|
|
2075
|
+
type: "object",
|
|
2076
|
+
properties: {
|
|
2077
|
+
issueKey: {
|
|
2078
|
+
type: "string",
|
|
2079
|
+
description: "The Jira issue key (e.g., MODS-123)",
|
|
2080
|
+
},
|
|
2081
|
+
accountId: {
|
|
2082
|
+
type: "string",
|
|
2083
|
+
description: "The account ID of the user to remove as watcher.",
|
|
2084
|
+
},
|
|
2085
|
+
instance: {
|
|
2086
|
+
type: "string",
|
|
2087
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
2088
|
+
},
|
|
2089
|
+
},
|
|
2090
|
+
required: ["issueKey", "accountId"],
|
|
2091
|
+
},
|
|
2092
|
+
},
|
|
2093
|
+
{
|
|
2094
|
+
name: "jira_add_worklog",
|
|
2095
|
+
description:
|
|
2096
|
+
"Log time/work on a Jira ticket. Use Jira time format for timeSpent (e.g., '2h 30m', '1d', '30m').",
|
|
2097
|
+
inputSchema: {
|
|
2098
|
+
type: "object",
|
|
2099
|
+
properties: {
|
|
2100
|
+
issueKey: {
|
|
2101
|
+
type: "string",
|
|
2102
|
+
description: "The Jira issue key (e.g., MODS-123)",
|
|
2103
|
+
},
|
|
2104
|
+
timeSpent: {
|
|
2105
|
+
type: "string",
|
|
2106
|
+
description: "Time spent in Jira format (e.g., '2h 30m', '1d', '4h')",
|
|
2107
|
+
},
|
|
2108
|
+
comment: {
|
|
2109
|
+
type: "string",
|
|
2110
|
+
description: "Optional work description/comment",
|
|
2111
|
+
},
|
|
2112
|
+
started: {
|
|
2113
|
+
type: "string",
|
|
2114
|
+
description: "When the work started in ISO 8601 format (e.g., '2026-04-08T09:00:00.000+0000'). Defaults to now.",
|
|
2115
|
+
},
|
|
2116
|
+
instance: {
|
|
2117
|
+
type: "string",
|
|
2118
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
2119
|
+
},
|
|
2120
|
+
},
|
|
2121
|
+
required: ["issueKey", "timeSpent"],
|
|
2122
|
+
},
|
|
2123
|
+
},
|
|
2124
|
+
{
|
|
2125
|
+
name: "jira_get_worklogs",
|
|
2126
|
+
description:
|
|
2127
|
+
"Get work logs (time tracking entries) for a Jira ticket.",
|
|
2128
|
+
inputSchema: {
|
|
2129
|
+
type: "object",
|
|
2130
|
+
properties: {
|
|
2131
|
+
issueKey: {
|
|
2132
|
+
type: "string",
|
|
2133
|
+
description: "The Jira issue key (e.g., MODS-123)",
|
|
2134
|
+
},
|
|
2135
|
+
instance: {
|
|
2136
|
+
type: "string",
|
|
2137
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
2138
|
+
},
|
|
2139
|
+
},
|
|
2140
|
+
required: ["issueKey"],
|
|
2141
|
+
},
|
|
2142
|
+
},
|
|
2143
|
+
{
|
|
2144
|
+
name: "jira_clone_ticket",
|
|
2145
|
+
description:
|
|
2146
|
+
"Clone/duplicate a Jira ticket. Creates a new ticket with the same summary, description, priority, labels, and components. Optionally links the clone to the original.",
|
|
2147
|
+
inputSchema: {
|
|
2148
|
+
type: "object",
|
|
2149
|
+
properties: {
|
|
2150
|
+
issueKey: {
|
|
2151
|
+
type: "string",
|
|
2152
|
+
description: "The issue key to clone (e.g., MODS-123)",
|
|
2153
|
+
},
|
|
2154
|
+
summaryPrefix: {
|
|
2155
|
+
type: "string",
|
|
2156
|
+
description: "Prefix to add to the cloned ticket summary (default: 'CLONE - ')",
|
|
2157
|
+
},
|
|
2158
|
+
linkToOriginal: {
|
|
2159
|
+
type: "boolean",
|
|
2160
|
+
description: "If true (default), creates a 'Cloners' link between the clone and original.",
|
|
2161
|
+
},
|
|
2162
|
+
targetProjectKey: {
|
|
2163
|
+
type: "string",
|
|
2164
|
+
description: "Project key for the clone. Defaults to the same project as the original.",
|
|
2165
|
+
},
|
|
2166
|
+
instance: {
|
|
2167
|
+
type: "string",
|
|
2168
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
2169
|
+
},
|
|
2170
|
+
},
|
|
2171
|
+
required: ["issueKey"],
|
|
2172
|
+
},
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
name: "jira_move_to_sprint",
|
|
2176
|
+
description:
|
|
2177
|
+
"Move one or more tickets into a sprint. Use jira_get_sprints to find sprint IDs.",
|
|
2178
|
+
inputSchema: {
|
|
2179
|
+
type: "object",
|
|
2180
|
+
properties: {
|
|
2181
|
+
sprintId: {
|
|
2182
|
+
type: "number",
|
|
2183
|
+
description: "The sprint ID to move tickets into. Use jira_get_sprints to find this.",
|
|
2184
|
+
},
|
|
2185
|
+
issueKeys: {
|
|
2186
|
+
type: "array",
|
|
2187
|
+
items: { type: "string" },
|
|
2188
|
+
description: "Array of issue keys to move (e.g., ['MODS-123', 'MODS-456'])",
|
|
2189
|
+
},
|
|
2190
|
+
instance: {
|
|
2191
|
+
type: "string",
|
|
2192
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
2193
|
+
},
|
|
2194
|
+
},
|
|
2195
|
+
required: ["sprintId", "issueKeys"],
|
|
2196
|
+
},
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
name: "jira_get_sprints",
|
|
2200
|
+
description:
|
|
2201
|
+
"List sprints for an agile board. Returns sprint IDs, names, states, and dates.",
|
|
2202
|
+
inputSchema: {
|
|
2203
|
+
type: "object",
|
|
2204
|
+
properties: {
|
|
2205
|
+
boardId: {
|
|
2206
|
+
type: "number",
|
|
2207
|
+
description: "The board ID. Use jira_get_boards to find this.",
|
|
2208
|
+
},
|
|
2209
|
+
state: {
|
|
2210
|
+
type: "string",
|
|
2211
|
+
description: "Filter by sprint state: 'active', 'future', 'closed', or comma-separated (e.g., 'active,future'). Defaults to all.",
|
|
2212
|
+
},
|
|
2213
|
+
instance: {
|
|
2214
|
+
type: "string",
|
|
2215
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
2216
|
+
},
|
|
2217
|
+
},
|
|
2218
|
+
required: ["boardId"],
|
|
2219
|
+
},
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
name: "jira_get_boards",
|
|
2223
|
+
description:
|
|
2224
|
+
"List agile boards (Scrum or Kanban). Optionally filter by project key or board name.",
|
|
2225
|
+
inputSchema: {
|
|
2226
|
+
type: "object",
|
|
2227
|
+
properties: {
|
|
2228
|
+
projectKeyOrId: {
|
|
2229
|
+
type: "string",
|
|
2230
|
+
description: "Filter boards by project key or ID (e.g., MODS)",
|
|
2231
|
+
},
|
|
2232
|
+
name: {
|
|
2233
|
+
type: "string",
|
|
2234
|
+
description: "Filter boards by name (partial match)",
|
|
2235
|
+
},
|
|
2236
|
+
type: {
|
|
2237
|
+
type: "string",
|
|
2238
|
+
description: "Filter by board type: 'scrum', 'kanban', or 'simple'",
|
|
2239
|
+
},
|
|
2240
|
+
instance: {
|
|
2241
|
+
type: "string",
|
|
2242
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
2243
|
+
},
|
|
2244
|
+
},
|
|
2245
|
+
required: [],
|
|
2246
|
+
},
|
|
2247
|
+
},
|
|
2248
|
+
{
|
|
2249
|
+
name: "jira_get_issue_types",
|
|
2250
|
+
description:
|
|
2251
|
+
"List available issue types for a project. Useful before creating tickets to know valid issue type names.",
|
|
2252
|
+
inputSchema: {
|
|
2253
|
+
type: "object",
|
|
2254
|
+
properties: {
|
|
2255
|
+
projectKey: {
|
|
2256
|
+
type: "string",
|
|
2257
|
+
description: "The project key (e.g., MODS)",
|
|
2258
|
+
},
|
|
2259
|
+
instance: {
|
|
2260
|
+
type: "string",
|
|
2261
|
+
description: "Instance name override. Auto-detected from project key if omitted.",
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2264
|
+
required: ["projectKey"],
|
|
2265
|
+
},
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
name: "jira_get_priorities",
|
|
2269
|
+
description:
|
|
2270
|
+
"List all available priority levels in the Jira instance.",
|
|
2271
|
+
inputSchema: {
|
|
2272
|
+
type: "object",
|
|
2273
|
+
properties: {
|
|
2274
|
+
instance: {
|
|
2275
|
+
type: "string",
|
|
2276
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
2277
|
+
},
|
|
2278
|
+
},
|
|
2279
|
+
required: [],
|
|
2280
|
+
},
|
|
2281
|
+
},
|
|
2282
|
+
{
|
|
2283
|
+
name: "jira_get_components",
|
|
2284
|
+
description:
|
|
2285
|
+
"List all components for a project. Useful before creating tickets to know valid component names.",
|
|
2286
|
+
inputSchema: {
|
|
2287
|
+
type: "object",
|
|
2288
|
+
properties: {
|
|
2289
|
+
projectKey: {
|
|
2290
|
+
type: "string",
|
|
2291
|
+
description: "The project key (e.g., MODS)",
|
|
2292
|
+
},
|
|
2293
|
+
instance: {
|
|
2294
|
+
type: "string",
|
|
2295
|
+
description: "Instance name override. Auto-detected from project key if omitted.",
|
|
2296
|
+
},
|
|
2297
|
+
},
|
|
2298
|
+
required: ["projectKey"],
|
|
2299
|
+
},
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: "jira_get_versions",
|
|
2303
|
+
description:
|
|
2304
|
+
"List all versions/releases for a project. Useful for setting fixVersions on tickets.",
|
|
2305
|
+
inputSchema: {
|
|
2306
|
+
type: "object",
|
|
2307
|
+
properties: {
|
|
2308
|
+
projectKey: {
|
|
2309
|
+
type: "string",
|
|
2310
|
+
description: "The project key (e.g., MODS)",
|
|
2311
|
+
},
|
|
2312
|
+
instance: {
|
|
2313
|
+
type: "string",
|
|
2314
|
+
description: "Instance name override. Auto-detected from project key if omitted.",
|
|
2315
|
+
},
|
|
2316
|
+
},
|
|
2317
|
+
required: ["projectKey"],
|
|
2318
|
+
},
|
|
2319
|
+
},
|
|
2320
|
+
],
|
|
2321
|
+
};
|
|
2322
|
+
});
|
|
2323
|
+
|
|
2324
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2325
|
+
const { name, arguments: args } = request.params;
|
|
2326
|
+
|
|
2327
|
+
try {
|
|
2328
|
+
if (name === "jira_get_myself") {
|
|
2329
|
+
const inst = getInstanceByName(args.instance);
|
|
2330
|
+
const result = await fetchJira("/myself", {}, inst);
|
|
2331
|
+
return {
|
|
2332
|
+
content: [
|
|
2333
|
+
{
|
|
2334
|
+
type: "text",
|
|
2335
|
+
text: `**Account ID:** ${result.accountId}\n**Display Name:** ${result.displayName}\n**Email:** ${result.emailAddress || "N/A"}`,
|
|
2336
|
+
},
|
|
2337
|
+
],
|
|
2338
|
+
};
|
|
2339
|
+
} else if (name === "jira_search_users") {
|
|
2340
|
+
const inst = getInstanceByName(args.instance);
|
|
2341
|
+
const maxResults = args.maxResults || 5;
|
|
2342
|
+
const users = await fetchJira(
|
|
2343
|
+
`/user/search?query=${encodeURIComponent(args.query)}&maxResults=${maxResults}`,
|
|
2344
|
+
{},
|
|
2345
|
+
inst,
|
|
2346
|
+
);
|
|
2347
|
+
if (!users || users.length === 0) {
|
|
2348
|
+
return {
|
|
2349
|
+
content: [
|
|
2350
|
+
{
|
|
2351
|
+
type: "text",
|
|
2352
|
+
text: `No users found for "${args.query}".`,
|
|
2353
|
+
},
|
|
2354
|
+
],
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
const lines = users.map(
|
|
2358
|
+
(u) =>
|
|
2359
|
+
`- **${u.displayName}** (accountId: ${u.accountId}${u.emailAddress ? `, email: ${u.emailAddress}` : ""})`,
|
|
2360
|
+
);
|
|
2361
|
+
return {
|
|
2362
|
+
content: [
|
|
2363
|
+
{
|
|
2364
|
+
type: "text",
|
|
2365
|
+
text: `Found ${users.length} user(s) for "${args.query}":\n\n${lines.join("\n")}`,
|
|
2366
|
+
},
|
|
2367
|
+
],
|
|
2368
|
+
};
|
|
2369
|
+
} else if (name === "jira_get_ticket") {
|
|
2370
|
+
if (!args.issueKey) {
|
|
2371
|
+
return { content: [{ type: "text", text: "Error: Missing required parameter 'issueKey'. You passed 'ticketId' which is not a valid parameter. Use 'issueKey' instead (e.g. issueKey: \"MODS-12115\")." }] };
|
|
2372
|
+
}
|
|
2373
|
+
const downloadImages = args.downloadImages !== false;
|
|
2374
|
+
const fetchFigma = args.fetchFigma !== false;
|
|
2375
|
+
const inst = args.instance ? getInstanceByName(args.instance) : null;
|
|
2376
|
+
const result = await getTicket(args.issueKey, downloadImages, fetchFigma, inst);
|
|
2377
|
+
|
|
2378
|
+
const content = [{ type: "text", text: result.text }];
|
|
2379
|
+
|
|
2380
|
+
// Add Jira images
|
|
2381
|
+
for (const imagePath of result.jiraImages) {
|
|
2382
|
+
try {
|
|
2383
|
+
const imageData = fs.readFileSync(imagePath);
|
|
2384
|
+
const ext = path.extname(imagePath).toLowerCase();
|
|
2385
|
+
const mimeType =
|
|
2386
|
+
ext === ".png"
|
|
2387
|
+
? "image/png"
|
|
2388
|
+
: ext === ".gif"
|
|
2389
|
+
? "image/gif"
|
|
2390
|
+
: ext === ".webp"
|
|
2391
|
+
? "image/webp"
|
|
2392
|
+
: "image/jpeg";
|
|
2393
|
+
content.push({
|
|
2394
|
+
type: "image",
|
|
2395
|
+
data: imageData.toString("base64"),
|
|
2396
|
+
mimeType: mimeType,
|
|
2397
|
+
});
|
|
2398
|
+
} catch (e) {
|
|
2399
|
+
/* skip */
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// Add Figma images (now supports multiple images per design)
|
|
2404
|
+
for (const design of result.figmaDesigns) {
|
|
2405
|
+
if (design.images && design.images.length > 0) {
|
|
2406
|
+
for (const img of design.images) {
|
|
2407
|
+
if (img.buffer) {
|
|
2408
|
+
content.push({
|
|
2409
|
+
type: "image",
|
|
2410
|
+
data: img.buffer.toString("base64"),
|
|
2411
|
+
mimeType: "image/png",
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
return { content };
|
|
2419
|
+
} else if (name === "jira_search") {
|
|
2420
|
+
const inst = getInstanceByName(args.instance);
|
|
2421
|
+
const result = await searchTickets(args.jql, args.maxResults || 10, args.fields || null, inst);
|
|
2422
|
+
return { content: [{ type: "text", text: result }] };
|
|
2423
|
+
} else if (name === "jira_add_comment") {
|
|
2424
|
+
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
2425
|
+
// Build ADF content with mention support
|
|
2426
|
+
const adfContent = await buildCommentADF(args.comment, inst);
|
|
2427
|
+
const body = {
|
|
2428
|
+
body: {
|
|
2429
|
+
version: 1,
|
|
2430
|
+
type: "doc",
|
|
2431
|
+
content: adfContent,
|
|
2432
|
+
},
|
|
2433
|
+
};
|
|
2434
|
+
const result = await fetchJira(`/issue/${args.issueKey}/comment`, {
|
|
2435
|
+
method: "POST",
|
|
2436
|
+
body,
|
|
2437
|
+
}, inst);
|
|
1706
2438
|
const author = result.author?.displayName || "Unknown";
|
|
1707
2439
|
const created = new Date(result.created).toLocaleString();
|
|
1708
2440
|
return {
|
|
@@ -1994,161 +2726,779 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1994
2726
|
fields.description = existing;
|
|
1995
2727
|
}
|
|
1996
2728
|
}
|
|
1997
|
-
if (args.assignee) {
|
|
1998
|
-
fields.assignee =
|
|
1999
|
-
args.assignee === "unassigned" ? null : { accountId: args.assignee };
|
|
2729
|
+
if (args.assignee) {
|
|
2730
|
+
fields.assignee =
|
|
2731
|
+
args.assignee === "unassigned" ? null : { accountId: args.assignee };
|
|
2732
|
+
}
|
|
2733
|
+
if (args.priority) {
|
|
2734
|
+
fields.priority = { name: args.priority };
|
|
2735
|
+
}
|
|
2736
|
+
if (args.labels) {
|
|
2737
|
+
fields.labels = args.labels;
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2740
|
+
if (Object.keys(fields).length === 0) {
|
|
2741
|
+
return {
|
|
2742
|
+
content: [{ type: "text", text: "No fields provided to update." }],
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
await fetchJira(`/issue/${args.issueKey}`, {
|
|
2747
|
+
method: "PUT",
|
|
2748
|
+
body: { fields },
|
|
2749
|
+
}, inst);
|
|
2750
|
+
const updated = Object.keys(fields).join(", ");
|
|
2751
|
+
return {
|
|
2752
|
+
content: [
|
|
2753
|
+
{ type: "text", text: `Updated ${args.issueKey}: ${updated}.` },
|
|
2754
|
+
],
|
|
2755
|
+
};
|
|
2756
|
+
} else if (name === "jira_get_changelog") {
|
|
2757
|
+
if (!args.issueKey && !args.jql) {
|
|
2758
|
+
return {
|
|
2759
|
+
content: [{ type: "text", text: "Error: Provide either issueKey or jql parameter." }],
|
|
2760
|
+
isError: true,
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
if (args.issueKey) {
|
|
2764
|
+
const result = await getChangelog(args.issueKey);
|
|
2765
|
+
return { content: [{ type: "text", text: result.formatted }] };
|
|
2766
|
+
} else {
|
|
2767
|
+
const inst = getInstanceByName(args.instance);
|
|
2768
|
+
const result = await getChangelogsBulk(args.jql, args.maxResults || 50, inst);
|
|
2769
|
+
return { content: [{ type: "text", text: result }] };
|
|
2770
|
+
}
|
|
2771
|
+
} else if (name === "jira_add_instance") {
|
|
2772
|
+
const instName = args.name.trim();
|
|
2773
|
+
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
2774
|
+
const isUpdate = existingIdx >= 0;
|
|
2775
|
+
|
|
2776
|
+
// For new instances, email/token/baseUrl are required
|
|
2777
|
+
if (!isUpdate && (!args.email || !args.token || !args.baseUrl)) {
|
|
2778
|
+
return {
|
|
2779
|
+
content: [{ type: "text", text: "New instance requires email, token, and baseUrl." }],
|
|
2780
|
+
isError: true,
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// Merge with existing or create new
|
|
2785
|
+
const existing = isUpdate ? instances[existingIdx] : {};
|
|
2786
|
+
const email = args.email || existing.email;
|
|
2787
|
+
const token = args.token || existing.token;
|
|
2788
|
+
const baseUrl = args.baseUrl ? args.baseUrl.replace(/\/$/, "") : existing.baseUrl;
|
|
2789
|
+
const projects = args.projects ? args.projects.map((p) => p.toUpperCase()) : (existing.projects || []);
|
|
2790
|
+
const authStr = Buffer.from(`${email}:${token}`).toString("base64");
|
|
2791
|
+
|
|
2792
|
+
const newInstance = { name: instName, email, token, baseUrl, projects, auth: authStr };
|
|
2793
|
+
|
|
2794
|
+
// Update in-memory instances
|
|
2795
|
+
if (isUpdate) {
|
|
2796
|
+
instances[existingIdx] = newInstance;
|
|
2797
|
+
} else {
|
|
2798
|
+
instances.push(newInstance);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
// Persist to config file
|
|
2802
|
+
const savedConfig = loadConfigFile();
|
|
2803
|
+
if (!savedConfig.instances) {
|
|
2804
|
+
// Migrate old format
|
|
2805
|
+
if (savedConfig.email) {
|
|
2806
|
+
savedConfig.instances = [{
|
|
2807
|
+
name: "default",
|
|
2808
|
+
email: savedConfig.email,
|
|
2809
|
+
token: savedConfig.token,
|
|
2810
|
+
baseUrl: savedConfig.baseUrl,
|
|
2811
|
+
projects: [],
|
|
2812
|
+
}];
|
|
2813
|
+
savedConfig.defaultInstance = "default";
|
|
2814
|
+
delete savedConfig.email;
|
|
2815
|
+
delete savedConfig.token;
|
|
2816
|
+
delete savedConfig.baseUrl;
|
|
2817
|
+
} else {
|
|
2818
|
+
savedConfig.instances = [];
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// Save without the computed auth field
|
|
2823
|
+
const toSave = { name: instName, email, token, baseUrl, projects };
|
|
2824
|
+
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
2825
|
+
if (savedIdx >= 0) {
|
|
2826
|
+
savedConfig.instances[savedIdx] = toSave;
|
|
2827
|
+
} else {
|
|
2828
|
+
savedConfig.instances.push(toSave);
|
|
2829
|
+
}
|
|
2830
|
+
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
2831
|
+
savedConfig.defaultInstance = instName;
|
|
2832
|
+
}
|
|
2833
|
+
fs.writeFileSync(jiraConfigPath, JSON.stringify(savedConfig, null, 2));
|
|
2834
|
+
|
|
2835
|
+
const action = isUpdate ? "Updated" : "Added";
|
|
2836
|
+
let text = `${action} instance "${instName}" (${baseUrl}).`;
|
|
2837
|
+
if (projects.length > 0) text += ` Projects: ${projects.join(", ")}.`;
|
|
2838
|
+
if (args.setDefault) text += " Set as default.";
|
|
2839
|
+
|
|
2840
|
+
return { content: [{ type: "text", text }] };
|
|
2841
|
+
|
|
2842
|
+
} else if (name === "jira_remove_instance") {
|
|
2843
|
+
const instName = args.name.trim();
|
|
2844
|
+
|
|
2845
|
+
if (instances.length <= 1) {
|
|
2846
|
+
return {
|
|
2847
|
+
content: [{ type: "text", text: "Cannot remove the last remaining instance." }],
|
|
2848
|
+
isError: true,
|
|
2849
|
+
};
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
const idx = instances.findIndex((i) => i.name === instName);
|
|
2853
|
+
if (idx < 0) {
|
|
2854
|
+
return {
|
|
2855
|
+
content: [{ type: "text", text: `Instance "${instName}" not found.` }],
|
|
2856
|
+
isError: true,
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
instances.splice(idx, 1);
|
|
2861
|
+
|
|
2862
|
+
// Persist to config file
|
|
2863
|
+
const savedConfig = loadConfigFile();
|
|
2864
|
+
if (savedConfig.instances) {
|
|
2865
|
+
savedConfig.instances = savedConfig.instances.filter((i) => i.name !== instName);
|
|
2866
|
+
if (savedConfig.defaultInstance === instName) {
|
|
2867
|
+
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
2868
|
+
}
|
|
2869
|
+
fs.writeFileSync(jiraConfigPath, JSON.stringify(savedConfig, null, 2));
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
return { content: [{ type: "text", text: `Removed instance "${instName}".` }] };
|
|
2873
|
+
|
|
2874
|
+
} else if (name === "jira_list_instances") {
|
|
2875
|
+
if (instances.length === 0) {
|
|
2876
|
+
return { content: [{ type: "text", text: "No instances configured." }] };
|
|
2877
|
+
}
|
|
2878
|
+
const currentDefault = rawConfig.defaultInstance || instances[0].name;
|
|
2879
|
+
let text = `# Configured Jira Instances (${instances.length})\n\n`;
|
|
2880
|
+
for (const inst of instances) {
|
|
2881
|
+
const isDefault = inst.name === currentDefault ? " **(default)**" : "";
|
|
2882
|
+
const projs = inst.projects?.length > 0 ? `\n Projects: ${inst.projects.join(", ")}` : "";
|
|
2883
|
+
text += `- **${inst.name}**${isDefault}: ${inst.baseUrl} (${inst.email})${projs}\n`;
|
|
2884
|
+
}
|
|
2885
|
+
return { content: [{ type: "text", text }] };
|
|
2886
|
+
|
|
2887
|
+
} else if (name === "jira_create_ticket") {
|
|
2888
|
+
const inst = args.instance
|
|
2889
|
+
? getInstanceByName(args.instance)
|
|
2890
|
+
: getInstanceForProject(args.projectKey);
|
|
2891
|
+
|
|
2892
|
+
const fields = {
|
|
2893
|
+
project: { key: args.projectKey.toUpperCase() },
|
|
2894
|
+
issuetype: { name: args.issueType },
|
|
2895
|
+
summary: args.summary,
|
|
2896
|
+
};
|
|
2897
|
+
|
|
2898
|
+
if (args.description) {
|
|
2899
|
+
const adfContent = await buildCommentADF(args.description, inst);
|
|
2900
|
+
fields.description = {
|
|
2901
|
+
version: 1,
|
|
2902
|
+
type: "doc",
|
|
2903
|
+
content: adfContent,
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
if (args.assignee) {
|
|
2907
|
+
fields.assignee = { accountId: args.assignee };
|
|
2908
|
+
}
|
|
2909
|
+
if (args.priority) {
|
|
2910
|
+
fields.priority = { name: args.priority };
|
|
2911
|
+
}
|
|
2912
|
+
if (args.labels) {
|
|
2913
|
+
fields.labels = args.labels;
|
|
2914
|
+
}
|
|
2915
|
+
if (args.components) {
|
|
2916
|
+
fields.components = args.components.map((c) => ({ name: c }));
|
|
2917
|
+
}
|
|
2918
|
+
if (args.fixVersions) {
|
|
2919
|
+
fields.fixVersions = args.fixVersions.map((v) => ({ name: v }));
|
|
2920
|
+
}
|
|
2921
|
+
if (args.storyPoints != null) {
|
|
2922
|
+
fields.customfield_10016 = args.storyPoints;
|
|
2923
|
+
}
|
|
2924
|
+
if (args.parentKey) {
|
|
2925
|
+
fields.parent = { key: args.parentKey };
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
const result = await fetchJira("/issue", { method: "POST", body: { fields } }, inst);
|
|
2929
|
+
const newKey = result.key;
|
|
2930
|
+
|
|
2931
|
+
if (args.sprintId) {
|
|
2932
|
+
await fetchJiraAgile(
|
|
2933
|
+
`/sprint/${args.sprintId}/issue`,
|
|
2934
|
+
{ method: "POST", body: { issues: [newKey] } },
|
|
2935
|
+
inst,
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
let text = `Created ${newKey}: ${args.summary}\nURL: ${inst.baseUrl}/browse/${newKey}`;
|
|
2940
|
+
if (args.sprintId) text += `\nAdded to sprint ${args.sprintId}.`;
|
|
2941
|
+
return { content: [{ type: "text", text }] };
|
|
2942
|
+
|
|
2943
|
+
} else if (name === "jira_create_subtask") {
|
|
2944
|
+
const inst = args.instance
|
|
2945
|
+
? getInstanceByName(args.instance)
|
|
2946
|
+
: getInstanceForKey(args.parentKey);
|
|
2947
|
+
const projectKey = args.parentKey.split("-")[0];
|
|
2948
|
+
|
|
2949
|
+
const fields = {
|
|
2950
|
+
project: { key: projectKey },
|
|
2951
|
+
parent: { key: args.parentKey },
|
|
2952
|
+
issuetype: { name: "Sub-task" },
|
|
2953
|
+
summary: args.summary,
|
|
2954
|
+
};
|
|
2955
|
+
|
|
2956
|
+
if (args.description) {
|
|
2957
|
+
const adfContent = await buildCommentADF(args.description, inst);
|
|
2958
|
+
fields.description = {
|
|
2959
|
+
version: 1,
|
|
2960
|
+
type: "doc",
|
|
2961
|
+
content: adfContent,
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
if (args.assignee) {
|
|
2965
|
+
fields.assignee = { accountId: args.assignee };
|
|
2966
|
+
}
|
|
2967
|
+
if (args.priority) {
|
|
2968
|
+
fields.priority = { name: args.priority };
|
|
2969
|
+
}
|
|
2970
|
+
if (args.labels) {
|
|
2971
|
+
fields.labels = args.labels;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
const result = await fetchJira("/issue", { method: "POST", body: { fields } }, inst);
|
|
2975
|
+
const newKey = result.key;
|
|
2976
|
+
return {
|
|
2977
|
+
content: [
|
|
2978
|
+
{
|
|
2979
|
+
type: "text",
|
|
2980
|
+
text: `Created subtask ${newKey} under ${args.parentKey}: ${args.summary}\nURL: ${inst.baseUrl}/browse/${newKey}`,
|
|
2981
|
+
},
|
|
2982
|
+
],
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
} else if (name === "jira_link_tickets") {
|
|
2986
|
+
const inst = args.instance
|
|
2987
|
+
? getInstanceByName(args.instance)
|
|
2988
|
+
: getInstanceForKey(args.inwardIssueKey);
|
|
2989
|
+
|
|
2990
|
+
const body = {
|
|
2991
|
+
type: { name: args.linkType },
|
|
2992
|
+
inwardIssue: { key: args.inwardIssueKey },
|
|
2993
|
+
outwardIssue: { key: args.outwardIssueKey },
|
|
2994
|
+
};
|
|
2995
|
+
|
|
2996
|
+
if (args.comment) {
|
|
2997
|
+
const adfContent = await buildCommentADF(args.comment, inst);
|
|
2998
|
+
body.comment = {
|
|
2999
|
+
body: {
|
|
3000
|
+
version: 1,
|
|
3001
|
+
type: "doc",
|
|
3002
|
+
content: adfContent,
|
|
3003
|
+
},
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
await fetchJira("/issueLink", { method: "POST", body }, inst);
|
|
3008
|
+
return {
|
|
3009
|
+
content: [
|
|
3010
|
+
{
|
|
3011
|
+
type: "text",
|
|
3012
|
+
text: `Linked ${args.inwardIssueKey} and ${args.outwardIssueKey} with "${args.linkType}".`,
|
|
3013
|
+
},
|
|
3014
|
+
],
|
|
3015
|
+
};
|
|
3016
|
+
|
|
3017
|
+
} else if (name === "jira_delete_ticket") {
|
|
3018
|
+
const inst = args.instance
|
|
3019
|
+
? getInstanceByName(args.instance)
|
|
3020
|
+
: getInstanceForKey(args.issueKey);
|
|
3021
|
+
const deleteSubtasks = args.deleteSubtasks ? "true" : "false";
|
|
3022
|
+
await fetchJira(
|
|
3023
|
+
`/issue/${args.issueKey}?deleteSubtasks=${deleteSubtasks}`,
|
|
3024
|
+
{ method: "DELETE" },
|
|
3025
|
+
inst,
|
|
3026
|
+
);
|
|
3027
|
+
return {
|
|
3028
|
+
content: [
|
|
3029
|
+
{
|
|
3030
|
+
type: "text",
|
|
3031
|
+
text: `Deleted ${args.issueKey}${args.deleteSubtasks ? " (including subtasks)" : ""}.`,
|
|
3032
|
+
},
|
|
3033
|
+
],
|
|
3034
|
+
};
|
|
3035
|
+
|
|
3036
|
+
} else if (name === "jira_add_attachment") {
|
|
3037
|
+
const inst = args.instance
|
|
3038
|
+
? getInstanceByName(args.instance)
|
|
3039
|
+
: getInstanceForKey(args.issueKey);
|
|
3040
|
+
|
|
3041
|
+
let fileBuffer;
|
|
3042
|
+
let fileName;
|
|
3043
|
+
|
|
3044
|
+
if (args.filePath) {
|
|
3045
|
+
if (!fs.existsSync(args.filePath)) {
|
|
3046
|
+
return {
|
|
3047
|
+
content: [{ type: "text", text: `Error: File not found: ${args.filePath}` }],
|
|
3048
|
+
isError: true,
|
|
3049
|
+
};
|
|
3050
|
+
}
|
|
3051
|
+
fileBuffer = fs.readFileSync(args.filePath);
|
|
3052
|
+
fileName = args.fileName || path.basename(args.filePath);
|
|
3053
|
+
} else if (args.fileContent) {
|
|
3054
|
+
if (!args.fileName) {
|
|
3055
|
+
return {
|
|
3056
|
+
content: [{ type: "text", text: "Error: fileName is required when using fileContent." }],
|
|
3057
|
+
isError: true,
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
fileBuffer = Buffer.from(args.fileContent, "base64");
|
|
3061
|
+
fileName = args.fileName;
|
|
3062
|
+
} else {
|
|
3063
|
+
return {
|
|
3064
|
+
content: [{ type: "text", text: "Error: Provide either filePath or fileContent." }],
|
|
3065
|
+
isError: true,
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
const boundary = "----JiraMCPBoundary" + Date.now();
|
|
3070
|
+
const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`;
|
|
3071
|
+
const footer = `\r\n--${boundary}--\r\n`;
|
|
3072
|
+
|
|
3073
|
+
const bodyBuffer = Buffer.concat([
|
|
3074
|
+
Buffer.from(header),
|
|
3075
|
+
fileBuffer,
|
|
3076
|
+
Buffer.from(footer),
|
|
3077
|
+
]);
|
|
3078
|
+
|
|
3079
|
+
const response = await fetch(
|
|
3080
|
+
`${inst.baseUrl}/rest/api/3/issue/${args.issueKey}/attachments`,
|
|
3081
|
+
{
|
|
3082
|
+
method: "POST",
|
|
3083
|
+
headers: {
|
|
3084
|
+
Authorization: `Basic ${inst.auth}`,
|
|
3085
|
+
"X-Atlassian-Token": "no-check",
|
|
3086
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
3087
|
+
},
|
|
3088
|
+
body: bodyBuffer,
|
|
3089
|
+
},
|
|
3090
|
+
);
|
|
3091
|
+
|
|
3092
|
+
if (!response.ok) {
|
|
3093
|
+
const errorBody = await response.text().catch(() => "");
|
|
3094
|
+
throw new Error(
|
|
3095
|
+
`Jira API error: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`,
|
|
3096
|
+
);
|
|
2000
3097
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
3098
|
+
|
|
3099
|
+
const result = await response.json();
|
|
3100
|
+
const attachments = Array.isArray(result) ? result : [result];
|
|
3101
|
+
const names = attachments.map((a) => a.filename).join(", ");
|
|
3102
|
+
return {
|
|
3103
|
+
content: [
|
|
3104
|
+
{
|
|
3105
|
+
type: "text",
|
|
3106
|
+
text: `Uploaded ${names} to ${args.issueKey}.`,
|
|
3107
|
+
},
|
|
3108
|
+
],
|
|
3109
|
+
};
|
|
3110
|
+
|
|
3111
|
+
} else if (name === "jira_get_link_types") {
|
|
3112
|
+
const inst = getInstanceByName(args.instance);
|
|
3113
|
+
const result = await fetchJira("/issueLinkType", {}, inst);
|
|
3114
|
+
const linkTypes = result.issueLinkTypes || [];
|
|
3115
|
+
if (linkTypes.length === 0) {
|
|
3116
|
+
return { content: [{ type: "text", text: "No link types found." }] };
|
|
2003
3117
|
}
|
|
2004
|
-
|
|
2005
|
-
|
|
3118
|
+
let text = `# Available Link Types (${linkTypes.length})\n\n`;
|
|
3119
|
+
for (const lt of linkTypes) {
|
|
3120
|
+
text += `- **${lt.name}** (id: ${lt.id})\n`;
|
|
3121
|
+
text += ` Inward: "${lt.inward}" | Outward: "${lt.outward}"\n`;
|
|
2006
3122
|
}
|
|
3123
|
+
return { content: [{ type: "text", text }] };
|
|
2007
3124
|
|
|
2008
|
-
|
|
3125
|
+
} else if (name === "jira_get_transitions") {
|
|
3126
|
+
const inst = args.instance
|
|
3127
|
+
? getInstanceByName(args.instance)
|
|
3128
|
+
: getInstanceForKey(args.issueKey);
|
|
3129
|
+
const result = await fetchJira(`/issue/${args.issueKey}/transitions`, {}, inst);
|
|
3130
|
+
const transitions = result.transitions || [];
|
|
3131
|
+
if (transitions.length === 0) {
|
|
3132
|
+
return { content: [{ type: "text", text: `No transitions available for ${args.issueKey}.` }] };
|
|
3133
|
+
}
|
|
3134
|
+
let text = `# Available Transitions for ${args.issueKey}\n\n`;
|
|
3135
|
+
for (const t of transitions) {
|
|
3136
|
+
text += `- **${t.name}** (id: ${t.id}) -> ${t.to?.name || "unknown"}\n`;
|
|
3137
|
+
}
|
|
3138
|
+
return { content: [{ type: "text", text }] };
|
|
3139
|
+
|
|
3140
|
+
} else if (name === "jira_unlink_tickets") {
|
|
3141
|
+
const inst = args.instance
|
|
3142
|
+
? getInstanceByName(args.instance)
|
|
3143
|
+
: getInstanceForKey(args.issueKey);
|
|
3144
|
+
const issue = await fetchJira(
|
|
3145
|
+
`/issue/${args.issueKey}?fields=issuelinks`,
|
|
3146
|
+
{},
|
|
3147
|
+
inst,
|
|
3148
|
+
);
|
|
3149
|
+
const links = issue.fields?.issuelinks || [];
|
|
3150
|
+
const link = links.find(
|
|
3151
|
+
(l) =>
|
|
3152
|
+
l.inwardIssue?.key === args.linkedIssueKey ||
|
|
3153
|
+
l.outwardIssue?.key === args.linkedIssueKey,
|
|
3154
|
+
);
|
|
3155
|
+
if (!link) {
|
|
2009
3156
|
return {
|
|
2010
|
-
content: [{ type: "text", text:
|
|
3157
|
+
content: [{ type: "text", text: `No link found between ${args.issueKey} and ${args.linkedIssueKey}.` }],
|
|
3158
|
+
isError: true,
|
|
2011
3159
|
};
|
|
2012
3160
|
}
|
|
2013
|
-
|
|
2014
|
-
await fetchJira(`/issue/${args.issueKey}`, {
|
|
2015
|
-
method: "PUT",
|
|
2016
|
-
body: { fields },
|
|
2017
|
-
}, inst);
|
|
2018
|
-
const updated = Object.keys(fields).join(", ");
|
|
3161
|
+
await fetchJira(`/issueLink/${link.id}`, { method: "DELETE" }, inst);
|
|
2019
3162
|
return {
|
|
2020
3163
|
content: [
|
|
2021
|
-
{
|
|
3164
|
+
{
|
|
3165
|
+
type: "text",
|
|
3166
|
+
text: `Removed link between ${args.issueKey} and ${args.linkedIssueKey}.`,
|
|
3167
|
+
},
|
|
2022
3168
|
],
|
|
2023
3169
|
};
|
|
2024
|
-
|
|
2025
|
-
|
|
3170
|
+
|
|
3171
|
+
} else if (name === "jira_remove_attachment") {
|
|
3172
|
+
if (args.attachmentId) {
|
|
3173
|
+
const inst = args.instance
|
|
3174
|
+
? getInstanceByName(args.instance)
|
|
3175
|
+
: (args.issueKey ? getInstanceForKey(args.issueKey) : getInstanceByName(undefined));
|
|
3176
|
+
await fetchJira(`/attachment/${args.attachmentId}`, { method: "DELETE" }, inst);
|
|
2026
3177
|
return {
|
|
2027
|
-
content: [{ type: "text", text:
|
|
2028
|
-
|
|
3178
|
+
content: [{ type: "text", text: `Deleted attachment ${args.attachmentId}.` }],
|
|
3179
|
+
};
|
|
3180
|
+
} else if (args.fileName && args.issueKey) {
|
|
3181
|
+
const inst = args.instance
|
|
3182
|
+
? getInstanceByName(args.instance)
|
|
3183
|
+
: getInstanceForKey(args.issueKey);
|
|
3184
|
+
const issue = await fetchJira(
|
|
3185
|
+
`/issue/${args.issueKey}?fields=attachment`,
|
|
3186
|
+
{},
|
|
3187
|
+
inst,
|
|
3188
|
+
);
|
|
3189
|
+
const attachments = issue.fields?.attachment || [];
|
|
3190
|
+
const att = attachments.find((a) => a.filename === args.fileName);
|
|
3191
|
+
if (!att) {
|
|
3192
|
+
return {
|
|
3193
|
+
content: [{ type: "text", text: `Attachment "${args.fileName}" not found on ${args.issueKey}.` }],
|
|
3194
|
+
isError: true,
|
|
3195
|
+
};
|
|
3196
|
+
}
|
|
3197
|
+
await fetchJira(`/attachment/${att.id}`, { method: "DELETE" }, inst);
|
|
3198
|
+
return {
|
|
3199
|
+
content: [{ type: "text", text: `Deleted attachment "${args.fileName}" from ${args.issueKey}.` }],
|
|
2029
3200
|
};
|
|
2030
|
-
}
|
|
2031
|
-
if (args.issueKey) {
|
|
2032
|
-
const result = await getChangelog(args.issueKey);
|
|
2033
|
-
return { content: [{ type: "text", text: result.formatted }] };
|
|
2034
3201
|
} else {
|
|
2035
|
-
const inst = getInstanceByName(args.instance);
|
|
2036
|
-
const result = await getChangelogsBulk(args.jql, args.maxResults || 50, inst);
|
|
2037
|
-
return { content: [{ type: "text", text: result }] };
|
|
2038
|
-
}
|
|
2039
|
-
} else if (name === "jira_add_instance") {
|
|
2040
|
-
const instName = args.name.trim();
|
|
2041
|
-
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
2042
|
-
const isUpdate = existingIdx >= 0;
|
|
2043
|
-
|
|
2044
|
-
// For new instances, email/token/baseUrl are required
|
|
2045
|
-
if (!isUpdate && (!args.email || !args.token || !args.baseUrl)) {
|
|
2046
3202
|
return {
|
|
2047
|
-
content: [{ type: "text", text: "
|
|
3203
|
+
content: [{ type: "text", text: "Error: Provide attachmentId, or both issueKey and fileName." }],
|
|
2048
3204
|
isError: true,
|
|
2049
3205
|
};
|
|
2050
3206
|
}
|
|
2051
3207
|
|
|
2052
|
-
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
3208
|
+
} else if (name === "jira_add_watcher") {
|
|
3209
|
+
const inst = args.instance
|
|
3210
|
+
? getInstanceByName(args.instance)
|
|
3211
|
+
: getInstanceForKey(args.issueKey);
|
|
3212
|
+
await fetchJira(
|
|
3213
|
+
`/issue/${args.issueKey}/watchers`,
|
|
3214
|
+
{ method: "POST", body: args.accountId },
|
|
3215
|
+
inst,
|
|
3216
|
+
);
|
|
3217
|
+
return {
|
|
3218
|
+
content: [{ type: "text", text: `Added watcher to ${args.issueKey}.` }],
|
|
3219
|
+
};
|
|
2059
3220
|
|
|
2060
|
-
|
|
3221
|
+
} else if (name === "jira_remove_watcher") {
|
|
3222
|
+
const inst = args.instance
|
|
3223
|
+
? getInstanceByName(args.instance)
|
|
3224
|
+
: getInstanceForKey(args.issueKey);
|
|
3225
|
+
await fetchJira(
|
|
3226
|
+
`/issue/${args.issueKey}/watchers?accountId=${encodeURIComponent(args.accountId)}`,
|
|
3227
|
+
{ method: "DELETE" },
|
|
3228
|
+
inst,
|
|
3229
|
+
);
|
|
3230
|
+
return {
|
|
3231
|
+
content: [{ type: "text", text: `Removed watcher from ${args.issueKey}.` }],
|
|
3232
|
+
};
|
|
2061
3233
|
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
3234
|
+
} else if (name === "jira_add_worklog") {
|
|
3235
|
+
const inst = args.instance
|
|
3236
|
+
? getInstanceByName(args.instance)
|
|
3237
|
+
: getInstanceForKey(args.issueKey);
|
|
3238
|
+
const body = { timeSpent: args.timeSpent };
|
|
3239
|
+
if (args.started) {
|
|
3240
|
+
body.started = args.started;
|
|
3241
|
+
}
|
|
3242
|
+
if (args.comment) {
|
|
3243
|
+
const adfContent = await buildCommentADF(args.comment, inst);
|
|
3244
|
+
body.comment = {
|
|
3245
|
+
version: 1,
|
|
3246
|
+
type: "doc",
|
|
3247
|
+
content: adfContent,
|
|
3248
|
+
};
|
|
2067
3249
|
}
|
|
3250
|
+
const result = await fetchJira(
|
|
3251
|
+
`/issue/${args.issueKey}/worklog`,
|
|
3252
|
+
{ method: "POST", body },
|
|
3253
|
+
inst,
|
|
3254
|
+
);
|
|
3255
|
+
return {
|
|
3256
|
+
content: [
|
|
3257
|
+
{
|
|
3258
|
+
type: "text",
|
|
3259
|
+
text: `Logged ${args.timeSpent} on ${args.issueKey} (worklog ID: ${result.id}).`,
|
|
3260
|
+
},
|
|
3261
|
+
],
|
|
3262
|
+
};
|
|
2068
3263
|
|
|
2069
|
-
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
3264
|
+
} else if (name === "jira_get_worklogs") {
|
|
3265
|
+
const inst = args.instance
|
|
3266
|
+
? getInstanceByName(args.instance)
|
|
3267
|
+
: getInstanceForKey(args.issueKey);
|
|
3268
|
+
const result = await fetchJira(`/issue/${args.issueKey}/worklog`, {}, inst);
|
|
3269
|
+
const worklogs = result.worklogs || [];
|
|
3270
|
+
if (worklogs.length === 0) {
|
|
3271
|
+
return { content: [{ type: "text", text: `No work logs for ${args.issueKey}.` }] };
|
|
3272
|
+
}
|
|
3273
|
+
let text = `# Work Logs for ${args.issueKey} (${worklogs.length})\n\n`;
|
|
3274
|
+
for (const w of worklogs) {
|
|
3275
|
+
const author = w.author?.displayName || "Unknown";
|
|
3276
|
+
const date = w.started ? w.started.substring(0, 10) : "N/A";
|
|
3277
|
+
const time = w.timeSpent || "N/A";
|
|
3278
|
+
text += `- **${author}** - ${time} on ${date}`;
|
|
3279
|
+
if (w.comment) {
|
|
3280
|
+
const commentText = typeof w.comment === "string" ? w.comment : extractText(w.comment);
|
|
3281
|
+
if (commentText) text += ` - ${commentText}`;
|
|
2087
3282
|
}
|
|
3283
|
+
text += ` (id: ${w.id})\n`;
|
|
2088
3284
|
}
|
|
3285
|
+
return { content: [{ type: "text", text }] };
|
|
2089
3286
|
|
|
2090
|
-
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
3287
|
+
} else if (name === "jira_clone_ticket") {
|
|
3288
|
+
const inst = args.instance
|
|
3289
|
+
? getInstanceByName(args.instance)
|
|
3290
|
+
: getInstanceForKey(args.issueKey);
|
|
3291
|
+
const original = await fetchJira(
|
|
3292
|
+
`/issue/${args.issueKey}?fields=summary,description,priority,labels,components,issuetype,project`,
|
|
3293
|
+
{},
|
|
3294
|
+
inst,
|
|
3295
|
+
);
|
|
3296
|
+
const of = original.fields;
|
|
3297
|
+
const prefix = args.summaryPrefix !== undefined ? args.summaryPrefix : "CLONE - ";
|
|
3298
|
+
const targetProject = args.targetProjectKey || of.project?.key;
|
|
3299
|
+
|
|
3300
|
+
const fields = {
|
|
3301
|
+
project: { key: targetProject },
|
|
3302
|
+
issuetype: { name: of.issuetype?.name || "Task" },
|
|
3303
|
+
summary: `${prefix}${of.summary}`,
|
|
3304
|
+
};
|
|
3305
|
+
if (of.description) {
|
|
3306
|
+
fields.description = of.description;
|
|
2097
3307
|
}
|
|
2098
|
-
if (
|
|
2099
|
-
|
|
3308
|
+
if (of.priority) {
|
|
3309
|
+
fields.priority = { name: of.priority.name };
|
|
3310
|
+
}
|
|
3311
|
+
if (of.labels?.length > 0) {
|
|
3312
|
+
fields.labels = of.labels;
|
|
3313
|
+
}
|
|
3314
|
+
if (of.components?.length > 0) {
|
|
3315
|
+
fields.components = of.components.map((c) => ({ name: c.name }));
|
|
2100
3316
|
}
|
|
2101
|
-
fs.writeFileSync(jiraConfigPath, JSON.stringify(savedConfig, null, 2));
|
|
2102
3317
|
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2105
|
-
if (projects.length > 0) text += ` Projects: ${projects.join(", ")}.`;
|
|
2106
|
-
if (args.setDefault) text += " Set as default.";
|
|
3318
|
+
const result = await fetchJira("/issue", { method: "POST", body: { fields } }, inst);
|
|
3319
|
+
const newKey = result.key;
|
|
2107
3320
|
|
|
2108
|
-
|
|
3321
|
+
const linkToOriginal = args.linkToOriginal !== false;
|
|
3322
|
+
if (linkToOriginal) {
|
|
3323
|
+
try {
|
|
3324
|
+
await fetchJira("/issueLink", {
|
|
3325
|
+
method: "POST",
|
|
3326
|
+
body: {
|
|
3327
|
+
type: { name: "Cloners" },
|
|
3328
|
+
inwardIssue: { key: newKey },
|
|
3329
|
+
outwardIssue: { key: args.issueKey },
|
|
3330
|
+
},
|
|
3331
|
+
}, inst);
|
|
3332
|
+
} catch {
|
|
3333
|
+
// Link type "Cloners" may not exist, try "Relates"
|
|
3334
|
+
try {
|
|
3335
|
+
await fetchJira("/issueLink", {
|
|
3336
|
+
method: "POST",
|
|
3337
|
+
body: {
|
|
3338
|
+
type: { name: "Relates" },
|
|
3339
|
+
inwardIssue: { key: newKey },
|
|
3340
|
+
outwardIssue: { key: args.issueKey },
|
|
3341
|
+
},
|
|
3342
|
+
}, inst);
|
|
3343
|
+
} catch {
|
|
3344
|
+
// Linking failed, but ticket was created
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
2109
3348
|
|
|
2110
|
-
|
|
2111
|
-
|
|
3349
|
+
return {
|
|
3350
|
+
content: [
|
|
3351
|
+
{
|
|
3352
|
+
type: "text",
|
|
3353
|
+
text: `Cloned ${args.issueKey} -> ${newKey}: ${prefix}${of.summary}\nURL: ${inst.baseUrl}/browse/${newKey}`,
|
|
3354
|
+
},
|
|
3355
|
+
],
|
|
3356
|
+
};
|
|
2112
3357
|
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
}
|
|
2118
|
-
|
|
3358
|
+
} else if (name === "jira_move_to_sprint") {
|
|
3359
|
+
const inst = getInstanceByName(args.instance);
|
|
3360
|
+
await fetchJiraAgile(
|
|
3361
|
+
`/sprint/${args.sprintId}/issue`,
|
|
3362
|
+
{ method: "POST", body: { issues: args.issueKeys } },
|
|
3363
|
+
inst,
|
|
3364
|
+
);
|
|
3365
|
+
return {
|
|
3366
|
+
content: [
|
|
3367
|
+
{
|
|
3368
|
+
type: "text",
|
|
3369
|
+
text: `Moved ${args.issueKeys.join(", ")} to sprint ${args.sprintId}.`,
|
|
3370
|
+
},
|
|
3371
|
+
],
|
|
3372
|
+
};
|
|
2119
3373
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
3374
|
+
} else if (name === "jira_get_sprints") {
|
|
3375
|
+
const inst = getInstanceByName(args.instance);
|
|
3376
|
+
let endpoint = `/board/${args.boardId}/sprint?maxResults=50`;
|
|
3377
|
+
if (args.state) {
|
|
3378
|
+
endpoint += `&state=${encodeURIComponent(args.state)}`;
|
|
3379
|
+
}
|
|
3380
|
+
const result = await fetchJiraAgile(endpoint, {}, inst);
|
|
3381
|
+
const sprints = result.values || [];
|
|
3382
|
+
if (sprints.length === 0) {
|
|
3383
|
+
return { content: [{ type: "text", text: `No sprints found for board ${args.boardId}.` }] };
|
|
2126
3384
|
}
|
|
3385
|
+
let text = `# Sprints for Board ${args.boardId} (${sprints.length})\n\n`;
|
|
3386
|
+
for (const s of sprints) {
|
|
3387
|
+
const start = s.startDate ? s.startDate.substring(0, 10) : "N/A";
|
|
3388
|
+
const end = s.endDate ? s.endDate.substring(0, 10) : "N/A";
|
|
3389
|
+
text += `- **${s.name}** (id: ${s.id}) - ${s.state} [${start} to ${end}]\n`;
|
|
3390
|
+
}
|
|
3391
|
+
return { content: [{ type: "text", text }] };
|
|
2127
3392
|
|
|
2128
|
-
|
|
3393
|
+
} else if (name === "jira_get_boards") {
|
|
3394
|
+
const inst = getInstanceByName(args.instance);
|
|
3395
|
+
let endpoint = "/board?maxResults=50";
|
|
3396
|
+
if (args.projectKeyOrId) {
|
|
3397
|
+
endpoint += `&projectKeyOrId=${encodeURIComponent(args.projectKeyOrId)}`;
|
|
3398
|
+
}
|
|
3399
|
+
if (args.name) {
|
|
3400
|
+
endpoint += `&name=${encodeURIComponent(args.name)}`;
|
|
3401
|
+
}
|
|
3402
|
+
if (args.type) {
|
|
3403
|
+
endpoint += `&type=${encodeURIComponent(args.type)}`;
|
|
3404
|
+
}
|
|
3405
|
+
const result = await fetchJiraAgile(endpoint, {}, inst);
|
|
3406
|
+
const boards = result.values || [];
|
|
3407
|
+
if (boards.length === 0) {
|
|
3408
|
+
return { content: [{ type: "text", text: "No boards found." }] };
|
|
3409
|
+
}
|
|
3410
|
+
let text = `# Agile Boards (${boards.length})\n\n`;
|
|
3411
|
+
for (const b of boards) {
|
|
3412
|
+
text += `- **${b.name}** (id: ${b.id}) - ${b.type}\n`;
|
|
3413
|
+
}
|
|
3414
|
+
return { content: [{ type: "text", text }] };
|
|
2129
3415
|
|
|
2130
|
-
|
|
2131
|
-
const
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
3416
|
+
} else if (name === "jira_get_issue_types") {
|
|
3417
|
+
const inst = args.instance
|
|
3418
|
+
? getInstanceByName(args.instance)
|
|
3419
|
+
: getInstanceForProject(args.projectKey);
|
|
3420
|
+
const result = await fetchJira(
|
|
3421
|
+
`/issue/createmeta?projectKeys=${encodeURIComponent(args.projectKey)}&expand=projects.issuetypes`,
|
|
3422
|
+
{},
|
|
3423
|
+
inst,
|
|
3424
|
+
);
|
|
3425
|
+
const project = result.projects?.[0];
|
|
3426
|
+
if (!project) {
|
|
3427
|
+
// Fallback: try the newer endpoint
|
|
3428
|
+
const types = await fetchJira(
|
|
3429
|
+
`/issuetype/project?projectId=${encodeURIComponent(args.projectKey)}`,
|
|
3430
|
+
{},
|
|
3431
|
+
inst,
|
|
3432
|
+
);
|
|
3433
|
+
const typeList = Array.isArray(types) ? types : [];
|
|
3434
|
+
if (typeList.length === 0) {
|
|
3435
|
+
return { content: [{ type: "text", text: `No issue types found for project ${args.projectKey}.` }] };
|
|
2136
3436
|
}
|
|
2137
|
-
|
|
3437
|
+
let text = `# Issue Types for ${args.projectKey} (${typeList.length})\n\n`;
|
|
3438
|
+
for (const t of typeList) {
|
|
3439
|
+
text += `- **${t.name}** (id: ${t.id})${t.subtask ? " [subtask]" : ""}${t.description ? ` - ${t.description}` : ""}\n`;
|
|
3440
|
+
}
|
|
3441
|
+
return { content: [{ type: "text", text }] };
|
|
3442
|
+
}
|
|
3443
|
+
const issueTypes = project.issuetypes || [];
|
|
3444
|
+
let text = `# Issue Types for ${args.projectKey} (${issueTypes.length})\n\n`;
|
|
3445
|
+
for (const t of issueTypes) {
|
|
3446
|
+
text += `- **${t.name}** (id: ${t.id})${t.subtask ? " [subtask]" : ""}${t.description ? ` - ${t.description}` : ""}\n`;
|
|
2138
3447
|
}
|
|
3448
|
+
return { content: [{ type: "text", text }] };
|
|
2139
3449
|
|
|
2140
|
-
|
|
3450
|
+
} else if (name === "jira_get_priorities") {
|
|
3451
|
+
const inst = getInstanceByName(args.instance);
|
|
3452
|
+
const result = await fetchJira("/priority", {}, inst);
|
|
3453
|
+
const priorities = Array.isArray(result) ? result : [];
|
|
3454
|
+
if (priorities.length === 0) {
|
|
3455
|
+
return { content: [{ type: "text", text: "No priorities found." }] };
|
|
3456
|
+
}
|
|
3457
|
+
let text = `# Available Priorities (${priorities.length})\n\n`;
|
|
3458
|
+
for (const p of priorities) {
|
|
3459
|
+
text += `- **${p.name}** (id: ${p.id})${p.description ? ` - ${p.description}` : ""}\n`;
|
|
3460
|
+
}
|
|
3461
|
+
return { content: [{ type: "text", text }] };
|
|
2141
3462
|
|
|
2142
|
-
} else if (name === "
|
|
2143
|
-
|
|
2144
|
-
|
|
3463
|
+
} else if (name === "jira_get_components") {
|
|
3464
|
+
const inst = args.instance
|
|
3465
|
+
? getInstanceByName(args.instance)
|
|
3466
|
+
: getInstanceForProject(args.projectKey);
|
|
3467
|
+
const result = await fetchJira(
|
|
3468
|
+
`/project/${encodeURIComponent(args.projectKey)}/components`,
|
|
3469
|
+
{},
|
|
3470
|
+
inst,
|
|
3471
|
+
);
|
|
3472
|
+
const components = Array.isArray(result) ? result : [];
|
|
3473
|
+
if (components.length === 0) {
|
|
3474
|
+
return { content: [{ type: "text", text: `No components found for project ${args.projectKey}.` }] };
|
|
2145
3475
|
}
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
3476
|
+
let text = `# Components for ${args.projectKey} (${components.length})\n\n`;
|
|
3477
|
+
for (const c of components) {
|
|
3478
|
+
const lead = c.lead?.displayName ? ` (lead: ${c.lead.displayName})` : "";
|
|
3479
|
+
text += `- **${c.name}** (id: ${c.id})${lead}${c.description ? ` - ${c.description}` : ""}\n`;
|
|
3480
|
+
}
|
|
3481
|
+
return { content: [{ type: "text", text }] };
|
|
3482
|
+
|
|
3483
|
+
} else if (name === "jira_get_versions") {
|
|
3484
|
+
const inst = args.instance
|
|
3485
|
+
? getInstanceByName(args.instance)
|
|
3486
|
+
: getInstanceForProject(args.projectKey);
|
|
3487
|
+
const result = await fetchJira(
|
|
3488
|
+
`/project/${encodeURIComponent(args.projectKey)}/versions`,
|
|
3489
|
+
{},
|
|
3490
|
+
inst,
|
|
3491
|
+
);
|
|
3492
|
+
const versions = Array.isArray(result) ? result : [];
|
|
3493
|
+
if (versions.length === 0) {
|
|
3494
|
+
return { content: [{ type: "text", text: `No versions found for project ${args.projectKey}.` }] };
|
|
3495
|
+
}
|
|
3496
|
+
let text = `# Versions for ${args.projectKey} (${versions.length})\n\n`;
|
|
3497
|
+
for (const v of versions) {
|
|
3498
|
+
const released = v.released ? " [released]" : "";
|
|
3499
|
+
const archived = v.archived ? " [archived]" : "";
|
|
3500
|
+
const date = v.releaseDate || "";
|
|
3501
|
+
text += `- **${v.name}** (id: ${v.id})${released}${archived}${date ? ` - ${date}` : ""}\n`;
|
|
2152
3502
|
}
|
|
2153
3503
|
return { content: [{ type: "text", text }] };
|
|
2154
3504
|
|
|
@@ -2168,4 +3518,11 @@ async function main() {
|
|
|
2168
3518
|
await server.connect(transport);
|
|
2169
3519
|
}
|
|
2170
3520
|
|
|
2171
|
-
|
|
3521
|
+
if (require.main === module) {
|
|
3522
|
+
main().catch(console.error);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// Export for testing
|
|
3526
|
+
if (typeof module !== "undefined") {
|
|
3527
|
+
module.exports = { buildCommentADF, parseInlineFormatting, findJiraTicketKeys };
|
|
3528
|
+
}
|