@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.
Files changed (2) hide show
  1. package/index.js +92 -23
  2. 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
- return nodes.length > 0 ? nodes : [{ type: "text", text: text }];
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
- // Add Jira images. Anthropic vision only accepts png/jpeg/gif/webp;
2509
- // SVG and other formats must be skipped or the API rejects the whole
2510
- // response with "Improperly formed request". Also skip files over 5MB.
2511
- const mimeByExt = {
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
- // Add Figma images (now supports multiple images per design)
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 && img.buffer.length <= MAX_IMAGE_BYTES) {
2541
- content.push({
2542
- type: "image",
2543
- data: img.buffer.toString("base64"),
2544
- mimeType: "image/png",
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.1",
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": {