@rui.branco/jira-mcp 1.7.1 → 1.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +92 -23
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -459,7 +459,73 @@ async function parseInlineFormatting(text, instance = defaultInstance) {
|
|
|
459
459
|
nodes.push({ type: "text", text: text.substring(lastIndex) });
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
|
|
462
|
+
const finalNodes = nodes.length > 0 ? nodes : [{ type: "text", text }];
|
|
463
|
+
return autoLinkTextNodes(finalNodes, instance);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Post-process plain text nodes: convert bare URLs and bare Jira ticket keys
|
|
467
|
+
// (e.g. PROJ-123) into ADF link nodes. Jira does NOT auto-link plain text in
|
|
468
|
+
// ADF — links must be explicit `link` marks.
|
|
469
|
+
function autoLinkTextNodes(nodes, instance = defaultInstance) {
|
|
470
|
+
const baseUrl = instance && instance.baseUrl;
|
|
471
|
+
const result = [];
|
|
472
|
+
// Bare URL OR Jira wiki-style [text|url] OR bare ticket key (PROJ-123)
|
|
473
|
+
const re = /(\[([^\]|\n]+)\|(https?:\/\/[^\]\s]+)\])|(https?:\/\/[^\s<>()\[\]]+)|(\b[A-Z][A-Z0-9]+-\d+\b)/g;
|
|
474
|
+
for (const node of nodes) {
|
|
475
|
+
if (node.type !== "text" || (node.marks && node.marks.length > 0)) {
|
|
476
|
+
result.push(node);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const text = node.text;
|
|
480
|
+
let last = 0;
|
|
481
|
+
let m;
|
|
482
|
+
re.lastIndex = 0;
|
|
483
|
+
while ((m = re.exec(text)) !== null) {
|
|
484
|
+
if (m.index > last) {
|
|
485
|
+
result.push({ type: "text", text: text.substring(last, m.index) });
|
|
486
|
+
}
|
|
487
|
+
if (m[1]) {
|
|
488
|
+
// [label|url] wiki markup
|
|
489
|
+
result.push({
|
|
490
|
+
type: "text",
|
|
491
|
+
text: m[2],
|
|
492
|
+
marks: [{ type: "link", attrs: { href: m[3] } }],
|
|
493
|
+
});
|
|
494
|
+
} else if (m[4]) {
|
|
495
|
+
// Bare URL — strip trailing punctuation that's unlikely to be part of the URL
|
|
496
|
+
let url = m[4];
|
|
497
|
+
const trailingMatch = url.match(/[.,;:!?]+$/);
|
|
498
|
+
let consumed = m[0].length;
|
|
499
|
+
if (trailingMatch) {
|
|
500
|
+
url = url.substring(0, url.length - trailingMatch[0].length);
|
|
501
|
+
consumed -= trailingMatch[0].length;
|
|
502
|
+
}
|
|
503
|
+
result.push({
|
|
504
|
+
type: "text",
|
|
505
|
+
text: url,
|
|
506
|
+
marks: [{ type: "link", attrs: { href: url } }],
|
|
507
|
+
});
|
|
508
|
+
last = m.index + consumed;
|
|
509
|
+
re.lastIndex = last;
|
|
510
|
+
continue;
|
|
511
|
+
} else if (m[5] && baseUrl) {
|
|
512
|
+
// Bare ticket key
|
|
513
|
+
const key = m[5];
|
|
514
|
+
result.push({
|
|
515
|
+
type: "text",
|
|
516
|
+
text: key,
|
|
517
|
+
marks: [{ type: "link", attrs: { href: `${baseUrl}/browse/${key}` } }],
|
|
518
|
+
});
|
|
519
|
+
} else if (m[5]) {
|
|
520
|
+
result.push({ type: "text", text: m[5] });
|
|
521
|
+
}
|
|
522
|
+
last = m.index + m[0].length;
|
|
523
|
+
}
|
|
524
|
+
if (last < text.length) {
|
|
525
|
+
result.push({ type: "text", text: text.substring(last) });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return result;
|
|
463
529
|
}
|
|
464
530
|
|
|
465
531
|
// Parse text with markdown formatting and @mentions, build ADF content
|
|
@@ -2505,24 +2571,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2505
2571
|
|
|
2506
2572
|
const content = [{ type: "text", text: result.text }];
|
|
2507
2573
|
|
|
2508
|
-
//
|
|
2509
|
-
//
|
|
2510
|
-
//
|
|
2511
|
-
|
|
2512
|
-
".png": "image/png",
|
|
2513
|
-
".jpg": "image/jpeg",
|
|
2514
|
-
".jpeg": "image/jpeg",
|
|
2515
|
-
".gif": "image/gif",
|
|
2516
|
-
".webp": "image/webp",
|
|
2517
|
-
};
|
|
2574
|
+
// Anthropic vision only accepts png/jpeg/gif/webp under 5MB. We sniff
|
|
2575
|
+
// the magic bytes instead of trusting the filename, since a misnamed
|
|
2576
|
+
// file (e.g. SVG saved as .png) gets the whole response rejected with
|
|
2577
|
+
// "Could not process image".
|
|
2518
2578
|
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
2579
|
+
const detectImageMime = (buf) => {
|
|
2580
|
+
if (!buf || buf.length < 12) return null;
|
|
2581
|
+
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return "image/png";
|
|
2582
|
+
if (buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return "image/jpeg";
|
|
2583
|
+
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x38) return "image/gif";
|
|
2584
|
+
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 &&
|
|
2585
|
+
buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) return "image/webp";
|
|
2586
|
+
return null;
|
|
2587
|
+
};
|
|
2519
2588
|
for (const imagePath of result.jiraImages) {
|
|
2520
2589
|
try {
|
|
2521
|
-
const ext = path.extname(imagePath).toLowerCase();
|
|
2522
|
-
const mimeType = mimeByExt[ext];
|
|
2523
|
-
if (!mimeType) continue;
|
|
2524
2590
|
const imageData = fs.readFileSync(imagePath);
|
|
2525
2591
|
if (imageData.length > MAX_IMAGE_BYTES) continue;
|
|
2592
|
+
const mimeType = detectImageMime(imageData);
|
|
2593
|
+
if (!mimeType) continue;
|
|
2526
2594
|
content.push({
|
|
2527
2595
|
type: "image",
|
|
2528
2596
|
data: imageData.toString("base64"),
|
|
@@ -2533,17 +2601,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2533
2601
|
}
|
|
2534
2602
|
}
|
|
2535
2603
|
|
|
2536
|
-
//
|
|
2604
|
+
// Figma images (multiple per design). Validate magic bytes here too.
|
|
2537
2605
|
for (const design of result.figmaDesigns) {
|
|
2538
2606
|
if (design.images && design.images.length > 0) {
|
|
2539
2607
|
for (const img of design.images) {
|
|
2540
|
-
if (img.buffer
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2608
|
+
if (!img.buffer || img.buffer.length > MAX_IMAGE_BYTES) continue;
|
|
2609
|
+
const mimeType = detectImageMime(img.buffer);
|
|
2610
|
+
if (!mimeType) continue;
|
|
2611
|
+
content.push({
|
|
2612
|
+
type: "image",
|
|
2613
|
+
data: img.buffer.toString("base64"),
|
|
2614
|
+
mimeType: mimeType,
|
|
2615
|
+
});
|
|
2547
2616
|
}
|
|
2548
2617
|
}
|
|
2549
2618
|
}
|
|
@@ -3828,5 +3897,5 @@ if (require.main === module) {
|
|
|
3828
3897
|
|
|
3829
3898
|
// Export for testing
|
|
3830
3899
|
if (typeof module !== "undefined") {
|
|
3831
|
-
module.exports = { buildCommentADF, parseInlineFormatting, findJiraTicketKeys, resolveTeamId, fetchJiraTeams, listTeams, searchTeamsViaJql };
|
|
3900
|
+
module.exports = { buildCommentADF, parseInlineFormatting, autoLinkTextNodes, findJiraTicketKeys, resolveTeamId, fetchJiraTeams, listTeams, searchTeamsViaJql };
|
|
3832
3901
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rui.branco/jira-mcp",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.3",
|
|
4
4
|
"description": "Jira MCP server for Claude Code - fetch tickets, search with JQL, update tickets, manage comments, change status, and get Figma designs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|