mcp-jira-stdio 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -338,6 +338,18 @@ jira_get_visible_projects({
338
338
  - Look for errors in Claude Desktop logs
339
339
  - Use `task inspector` to debug
340
340
 
341
+ **Timeout when running multiple instances with npx**
342
+
343
+ If you're running multiple Claude Code sessions simultaneously and experience timeouts, this is caused by `npx` cache/registry locking — not the MCP server itself. Each instance tries to verify the package, causing conflicts. To fix this, install the package globally instead:
344
+
345
+ ```bash
346
+ npm install -g mcp-jira-stdio
347
+ claude mcp add jira mcp-jira-stdio \
348
+ --env JIRA_BASE_URL=... \
349
+ --env JIRA_EMAIL=... \
350
+ --env JIRA_API_TOKEN=...
351
+ ```
352
+
341
353
  ### Debug Commands
342
354
 
343
355
  ```bash
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
4
4
  import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
5
  import axios from 'axios';
6
6
  import { z } from 'zod';
7
+ import FormData from 'form-data';
7
8
  import mdToAdf from 'md-to-adf';
8
9
 
9
10
  var __create = Object.create;
@@ -440,7 +441,7 @@ var require_package2 = __commonJS({
440
441
  "package.json"(exports$1, module) {
441
442
  module.exports = {
442
443
  name: "mcp-jira-stdio",
443
- version: "1.7.0",
444
+ version: "1.8.0",
444
445
  description: "Model Context Protocol (MCP) server for Jira API integration",
445
446
  author: "graslt",
446
447
  license: "MIT",
@@ -491,6 +492,7 @@ var require_package2 = __commonJS({
491
492
  dependencies: {
492
493
  "@modelcontextprotocol/sdk": "^1.17.1",
493
494
  axios: "^1.7.0",
495
+ "form-data": "^4.0.5",
494
496
  "md-to-adf": "^0.5.0",
495
497
  zod: "^3.22.4"
496
498
  },
@@ -548,6 +550,13 @@ var ERROR_MESSAGES = {
548
550
  NETWORK_ERROR: "Network error occurred while connecting to Jira.",
549
551
  VALIDATION_ERROR: "Input validation failed."
550
552
  };
553
+ var ATTACHMENT_CONFIG = {
554
+ MAX_FILE_SIZE: 50 * 1024 * 1024,
555
+ // 50 MB - Jira attachment size limit
556
+ MAX_BASE64_SIZE: 10 * 1024 * 1024,
557
+ // 10 MB for base64 content (token limit warning)
558
+ ALLOWED_PROTOCOLS: ["http:", "https:"]
559
+ };
551
560
  var TOOL_NAMES = {
552
561
  GET_VISIBLE_PROJECTS: "jira_get_visible_projects",
553
562
  GET_ISSUE: "jira_get_issue",
@@ -564,7 +573,11 @@ var TOOL_NAMES = {
564
573
  CREATE_SUBTASK: "jira_create_subtask",
565
574
  GET_CREATE_META: "jira_get_create_meta",
566
575
  GET_CUSTOM_FIELDS: "jira_get_custom_fields",
567
- CREATE_ISSUE_LINK: "jira_create_issue_link"
576
+ CREATE_ISSUE_LINK: "jira_create_issue_link",
577
+ GET_COMMENTS: "jira_get_comments",
578
+ ADD_ATTACHMENT: "jira_add_attachment",
579
+ GET_ATTACHMENTS: "jira_get_attachments",
580
+ DELETE_ATTACHMENT: "jira_delete_attachment"
568
581
  };
569
582
 
570
583
  // src/utils/logger.ts
@@ -604,6 +617,7 @@ function createLogger(scope) {
604
617
 
605
618
  // src/utils/jira-auth.ts
606
619
  var jiraClient = null;
620
+ var jiraMultipartClient = null;
607
621
  var log = createLogger("jira-auth");
608
622
  function validateAuth() {
609
623
  const baseUrl = process.env.JIRA_BASE_URL;
@@ -706,6 +720,96 @@ async function makeJiraRequest(config, retryConfig = {
706
720
  }
707
721
  throw lastError;
708
722
  }
723
+ function getMultipartClient() {
724
+ if (jiraMultipartClient) {
725
+ return jiraMultipartClient;
726
+ }
727
+ const auth = validateAuth();
728
+ jiraMultipartClient = axios.create({
729
+ baseURL: `${auth.baseUrl}${JIRA_CONFIG.BASE_PATH}`,
730
+ timeout: JIRA_CONFIG.DEFAULT_TIMEOUT,
731
+ headers: {
732
+ Accept: "application/json",
733
+ "X-Atlassian-Token": "no-check"
734
+ },
735
+ auth: {
736
+ username: auth.email,
737
+ password: auth.apiToken
738
+ }
739
+ });
740
+ jiraMultipartClient.interceptors.request.use(
741
+ (config) => {
742
+ log.debug(
743
+ `Multipart Request: ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`
744
+ );
745
+ return config;
746
+ },
747
+ (error) => {
748
+ log.error("Multipart request error:", error);
749
+ return Promise.reject(error);
750
+ }
751
+ );
752
+ jiraMultipartClient.interceptors.response.use(
753
+ (response) => {
754
+ log.debug(`Multipart Response: ${response.status} ${response.config.url}`);
755
+ return response;
756
+ },
757
+ (error) => {
758
+ log.error(`Jira API multipart error: ${error.response?.status} ${error.config?.url}`);
759
+ if (error.response?.status === 401) {
760
+ throw new Error(ERROR_MESSAGES.INVALID_CREDENTIALS);
761
+ } else if (error.response?.status === 404) {
762
+ throw new Error("Resource not found or insufficient permissions");
763
+ } else if (error.response?.status === 429) {
764
+ throw new Error(ERROR_MESSAGES.RATE_LIMIT_EXCEEDED);
765
+ } else if (!error.response) {
766
+ throw new Error(ERROR_MESSAGES.NETWORK_ERROR);
767
+ }
768
+ return Promise.reject(error);
769
+ }
770
+ );
771
+ return jiraMultipartClient;
772
+ }
773
+ async function makeMultipartRequest(url, formData, retryConfig = {
774
+ maxRetries: JIRA_CONFIG.MAX_RETRIES,
775
+ retryDelay: JIRA_CONFIG.RETRY_DELAY
776
+ }) {
777
+ const client = getMultipartClient();
778
+ let lastError;
779
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
780
+ try {
781
+ const response = await client.post(url, formData, {
782
+ headers: {
783
+ ...formData.getHeaders()
784
+ }
785
+ });
786
+ return response.data;
787
+ } catch (error) {
788
+ lastError = error;
789
+ if (error.response?.status === 401 || error.response?.status === 403 || error.response?.status === 404 || error.response?.status >= 400 && error.response?.status < 500 && error.response?.status !== 429) {
790
+ break;
791
+ }
792
+ if (attempt === retryConfig.maxRetries) {
793
+ break;
794
+ }
795
+ const retryAfterHeader = error.response?.headers?.["retry-after"];
796
+ let delayMs = 0;
797
+ if (retryAfterHeader) {
798
+ const seconds = Number(retryAfterHeader);
799
+ delayMs = !Number.isNaN(seconds) ? seconds * 1e3 : retryConfig.retryDelay;
800
+ } else {
801
+ const base = retryConfig.retryDelay * Math.pow(2, attempt);
802
+ const jitter = Math.floor(Math.random() * (retryConfig.retryDelay / 2));
803
+ delayMs = base + jitter;
804
+ }
805
+ log.warn(
806
+ `Retrying multipart request in ${delayMs}ms (attempt ${attempt + 1}/${retryConfig.maxRetries})`
807
+ );
808
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
809
+ }
810
+ }
811
+ throw lastError;
812
+ }
709
813
  function validateInput(schema, input) {
710
814
  try {
711
815
  return schema.parse(input);
@@ -848,6 +952,39 @@ var CreateIssueLinkInputSchema = z.object({
848
952
  'Link type: "blocks", "is blocked by", "relates", "duplicates", "clones", or custom link type name'
849
953
  )
850
954
  });
955
+ var GetCommentsInputSchema = z.object({
956
+ issueKey: z.string().describe("Issue key to get comments for (e.g., PROJECT-123)").refine((v) => isValidIssueKey(v), "Invalid issue key format"),
957
+ maxResults: z.number().min(1).max(100).optional().describe("Maximum number of comments to return (default: 50)"),
958
+ orderBy: z.enum(["created", "-created", "+created"]).optional().describe('Sort order for comments: "created" (oldest first), "-created" (newest first)'),
959
+ startAt: z.number().min(0).optional().describe("Index of first comment to return (for pagination)")
960
+ });
961
+ var AddAttachmentInputSchema = z.object({
962
+ issueKey: z.string().describe("Issue key to add attachment to (e.g., PROJECT-123)").refine((v) => isValidIssueKey(v), "Invalid issue key format"),
963
+ filename: z.string().min(1).describe("Name of the file to attach"),
964
+ // Option 1: Remote URL (efficient - ~60 tokens)
965
+ fileUrl: z.string().url().optional().describe(
966
+ "URL to download file from (efficient for remote files, ~60 tokens). Upload large files to Dropbox/S3/imgur first, then use URL."
967
+ ),
968
+ // Option 2: Base64 content (for small files or backwards compatibility)
969
+ content: z.string().optional().describe(
970
+ "Base64-encoded or plain text content. WARNING: HIGH token cost (~330k tokens for 1MB file). Use fileUrl for large files or upload to cloud storage first."
971
+ ),
972
+ isBase64: z.boolean().optional().default(true).describe(
973
+ "Whether content is base64-encoded (default: true). Set to false for plain text files."
974
+ )
975
+ }).refine(
976
+ (data) => {
977
+ const sources = [data.fileUrl, data.content].filter(Boolean);
978
+ return sources.length === 1;
979
+ },
980
+ { message: "Exactly one of fileUrl or content must be provided" }
981
+ );
982
+ var GetAttachmentsInputSchema = z.object({
983
+ issueKey: z.string().describe("Issue key to get attachments for (e.g., PROJECT-123)").refine((v) => isValidIssueKey(v), "Invalid issue key format")
984
+ });
985
+ var DeleteAttachmentInputSchema = z.object({
986
+ attachmentId: z.string().min(1).describe("ID of the attachment to delete")
987
+ });
851
988
  function ensureAdfDescription(desc, format = "markdown") {
852
989
  if (!desc) return desc;
853
990
  if (typeof desc === "object") return desc;
@@ -1198,6 +1335,24 @@ async function addComment(issueKey, body, visibility, format) {
1198
1335
  };
1199
1336
  return await makeJiraRequest(config);
1200
1337
  }
1338
+ async function getComments(issueKey, options = {}) {
1339
+ const params = {};
1340
+ if (options.maxResults !== void 0) {
1341
+ params.maxResults = options.maxResults;
1342
+ }
1343
+ if (options.orderBy !== void 0) {
1344
+ params.orderBy = options.orderBy;
1345
+ }
1346
+ if (options.startAt !== void 0) {
1347
+ params.startAt = options.startAt;
1348
+ }
1349
+ const config = {
1350
+ method: "GET",
1351
+ url: `/issue/${issueKey}/comment`,
1352
+ params
1353
+ };
1354
+ return await makeJiraRequest(config);
1355
+ }
1201
1356
  async function getProjectDetails(projectKey, expand) {
1202
1357
  const params = {};
1203
1358
  if (expand) {
@@ -1309,6 +1464,63 @@ async function createIssueLink(fromIssue, toIssue, linkType) {
1309
1464
  };
1310
1465
  await makeJiraRequest(config);
1311
1466
  }
1467
+ async function addAttachment(issueKey, filename, content, isBase64 = true) {
1468
+ const formData = new FormData();
1469
+ let fileBuffer;
1470
+ if (isBase64) {
1471
+ fileBuffer = Buffer.from(content, "base64");
1472
+ } else {
1473
+ fileBuffer = Buffer.from(content, "utf-8");
1474
+ }
1475
+ formData.append("file", fileBuffer, {
1476
+ filename,
1477
+ contentType: "application/octet-stream"
1478
+ });
1479
+ return await makeMultipartRequest(`/issue/${issueKey}/attachments`, formData);
1480
+ }
1481
+ async function addAttachmentFromUrl(issueKey, fileUrl, filename) {
1482
+ const axios2 = (await import('axios')).default;
1483
+ const urlObj = new URL(fileUrl);
1484
+ if (urlObj.hostname === "localhost" || urlObj.hostname.startsWith("127.")) {
1485
+ throw new Error("Cannot download from localhost (SSRF protection)");
1486
+ }
1487
+ if (urlObj.hostname.startsWith("192.168.") || urlObj.hostname.startsWith("10.")) {
1488
+ throw new Error("Cannot download from private network (SSRF protection)");
1489
+ }
1490
+ if (urlObj.hostname.startsWith("172.")) {
1491
+ const parts = urlObj.hostname.split(".");
1492
+ const secondOctet = parts[1] ? parseInt(parts[1]) : 0;
1493
+ if (secondOctet >= 16 && secondOctet <= 31) {
1494
+ throw new Error("Cannot download from private network (SSRF protection)");
1495
+ }
1496
+ }
1497
+ const response = await axios2.get(fileUrl, {
1498
+ responseType: "arraybuffer",
1499
+ maxContentLength: 50 * 1024 * 1024,
1500
+ // 50 MB limit
1501
+ timeout: 3e4
1502
+ // 30 seconds
1503
+ });
1504
+ const fileBuffer = Buffer.from(response.data);
1505
+ const formData = new FormData();
1506
+ formData.append("file", fileBuffer, {
1507
+ filename,
1508
+ contentType: response.headers["content-type"] || "application/octet-stream"
1509
+ });
1510
+ return await makeMultipartRequest(`/issue/${issueKey}/attachments`, formData);
1511
+ }
1512
+ async function getAttachments(issueKey) {
1513
+ const issue = await getIssue(issueKey, { fields: ["attachment"] });
1514
+ const attachments = issue.fields.attachment || [];
1515
+ return attachments;
1516
+ }
1517
+ async function deleteAttachment(attachmentId) {
1518
+ const config = {
1519
+ method: "DELETE",
1520
+ url: `/attachment/${attachmentId}`
1521
+ };
1522
+ await makeJiraRequest(config);
1523
+ }
1312
1524
 
1313
1525
  // src/utils/redaction.ts
1314
1526
  function isRedactionEnabled() {
@@ -1631,6 +1843,44 @@ ${comment.body}`
1631
1843
  ]
1632
1844
  };
1633
1845
  }
1846
+ function formatCommentsResponse(response, issueKey) {
1847
+ if (!response.comments || response.comments.length === 0) {
1848
+ return {
1849
+ content: [
1850
+ {
1851
+ type: "text",
1852
+ text: `No comments found for issue ${issueKey}.`
1853
+ }
1854
+ ]
1855
+ };
1856
+ }
1857
+ const commentsList = response.comments.map((comment) => {
1858
+ const bodyText = typeof comment.body === "string" ? comment.body : adfToPlainText(comment.body);
1859
+ const visibilityText = comment.visibility ? ` [${comment.visibility.type}: ${comment.visibility.value}]` : "";
1860
+ const authorName = comment.author?.displayName || "Unknown";
1861
+ const createdDate = comment.created ? new Date(comment.created).toISOString() : "N/A";
1862
+ const updatedDate = comment.updated ? new Date(comment.updated).toISOString() : "N/A";
1863
+ const wasEdited = comment.created !== comment.updated;
1864
+ return `**${authorName}** - ${createdDate}${wasEdited ? ` (edited: ${updatedDate})` : ""}${visibilityText}
1865
+ ${bodyText}`;
1866
+ }).join("\n\n---\n\n");
1867
+ const total = response.total;
1868
+ const showingStart = response.startAt + 1;
1869
+ const showingEnd = Math.min(response.startAt + response.comments.length, total);
1870
+ const paginationInfo = total > response.comments.length ? `
1871
+
1872
+ *Showing ${showingStart}-${showingEnd} of ${total} comments*` : "";
1873
+ return {
1874
+ content: [
1875
+ {
1876
+ type: "text",
1877
+ text: `**Comments for ${issueKey}** (${response.comments.length} comment${response.comments.length !== 1 ? "s" : ""}):
1878
+
1879
+ ${commentsList}${paginationInfo}`
1880
+ }
1881
+ ]
1882
+ };
1883
+ }
1634
1884
  function formatProjectDetailsResponse(project) {
1635
1885
  const leadText = project.lead ? `${project.lead.displayName} (${project.lead.accountId})` : "No lead assigned";
1636
1886
  const componentsText = project.components.length > 0 ? project.components.map((c) => c.name).join(", ") : "None";
@@ -1751,6 +2001,67 @@ ${fieldsList}`
1751
2001
  ]
1752
2002
  };
1753
2003
  }
2004
+ function formatFileSize(bytes) {
2005
+ if (bytes < 1024) return `${bytes} B`;
2006
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2007
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2008
+ }
2009
+ function formatAttachmentResponse(attachment, issueKey) {
2010
+ const authorName = attachment.author?.displayName || "Unknown";
2011
+ const createdDate = attachment.created ? new Date(attachment.created).toISOString() : "N/A";
2012
+ return {
2013
+ content: [
2014
+ {
2015
+ type: "text",
2016
+ text: `**Attachment uploaded successfully to ${issueKey}**
2017
+
2018
+ **Filename:** ${attachment.filename}
2019
+ **ID:** ${attachment.id}
2020
+ **Size:** ${formatFileSize(attachment.size)}
2021
+ **MIME Type:** ${attachment.mimeType}
2022
+ **Author:** ${authorName}
2023
+ **Created:** ${createdDate}
2024
+ **Download URL:** ${attachment.content}
2025
+ ${attachment.thumbnail ? `**Thumbnail:** ${attachment.thumbnail}` : ""}
2026
+
2027
+ \u{1F4A1} **Tip:** To embed this image in a comment or description, use: \`!${attachment.filename}!\` or \`!${attachment.filename}|thumbnail!\``
2028
+ }
2029
+ ]
2030
+ };
2031
+ }
2032
+ function formatAttachmentsListResponse(attachments, issueKey) {
2033
+ if (!attachments || attachments.length === 0) {
2034
+ return {
2035
+ content: [
2036
+ {
2037
+ type: "text",
2038
+ text: `No attachments found for issue ${issueKey}.`
2039
+ }
2040
+ ]
2041
+ };
2042
+ }
2043
+ const attachmentsList = attachments.map((attachment) => {
2044
+ const authorName = attachment.author?.displayName || "Unknown";
2045
+ const createdDate = attachment.created ? new Date(attachment.created).toISOString() : "N/A";
2046
+ const thumbnailInfo = attachment.thumbnail ? " (has thumbnail)" : "";
2047
+ return `\u2022 **${attachment.filename}** (ID: ${attachment.id})
2048
+ Size: ${formatFileSize(attachment.size)} | Type: ${attachment.mimeType}${thumbnailInfo}
2049
+ Author: ${authorName} | Created: ${createdDate}
2050
+ URL: ${attachment.content}`;
2051
+ }).join("\n\n");
2052
+ return {
2053
+ content: [
2054
+ {
2055
+ type: "text",
2056
+ text: `**Attachments for ${issueKey}** (${attachments.length} attachment${attachments.length !== 1 ? "s" : ""}):
2057
+
2058
+ ${attachmentsList}
2059
+
2060
+ \u{1F4A1} **Tip:** To embed an image in a comment or description, use: \`!filename.png!\` or \`!filename.png|thumbnail!\``
2061
+ }
2062
+ ]
2063
+ };
2064
+ }
1754
2065
 
1755
2066
  // src/utils/error-handler.ts
1756
2067
  var log2 = createLogger("error-handler");
@@ -2622,6 +2933,203 @@ async function handleCreateIssueLink(input) {
2622
2933
  }
2623
2934
  }
2624
2935
 
2936
+ // src/tools/get-comments.ts
2937
+ var log19 = createLogger("tool:get-comments");
2938
+ var getCommentsTool = {
2939
+ name: TOOL_NAMES.GET_COMMENTS,
2940
+ description: "Retrieves all comments for a Jira issue. Returns comment author, content, timestamps, and visibility settings. Supports pagination for issues with many comments.",
2941
+ inputSchema: {
2942
+ type: "object",
2943
+ properties: {
2944
+ issueKey: {
2945
+ type: "string",
2946
+ description: "Issue key to get comments for (e.g., PROJECT-123)"
2947
+ },
2948
+ maxResults: {
2949
+ type: "number",
2950
+ description: "Maximum number of comments to return (default: 50, max: 100)",
2951
+ minimum: 1,
2952
+ maximum: 100
2953
+ },
2954
+ orderBy: {
2955
+ type: "string",
2956
+ enum: ["created", "-created", "+created"],
2957
+ description: 'Sort order for comments: "created" or "+created" (oldest first), "-created" (newest first)'
2958
+ },
2959
+ startAt: {
2960
+ type: "number",
2961
+ description: "Index of first comment to return (for pagination, default: 0)",
2962
+ minimum: 0
2963
+ }
2964
+ },
2965
+ required: ["issueKey"]
2966
+ }
2967
+ };
2968
+ async function handleGetComments(input) {
2969
+ try {
2970
+ const validated = validateInput(GetCommentsInputSchema, input);
2971
+ log19.info(`Getting comments for issue ${validated.issueKey}...`);
2972
+ const options = {};
2973
+ if (validated.maxResults !== void 0) {
2974
+ options.maxResults = validated.maxResults;
2975
+ }
2976
+ if (validated.orderBy !== void 0) {
2977
+ options.orderBy = validated.orderBy;
2978
+ }
2979
+ if (validated.startAt !== void 0) {
2980
+ options.startAt = validated.startAt;
2981
+ }
2982
+ const comments = await getComments(validated.issueKey, options);
2983
+ log19.info(`Retrieved ${comments.comments.length} comment(s) for ${validated.issueKey}`);
2984
+ return formatCommentsResponse(comments, validated.issueKey);
2985
+ } catch (error) {
2986
+ log19.error("Error in handleGetComments:", error);
2987
+ return handleError(error);
2988
+ }
2989
+ }
2990
+
2991
+ // src/tools/add-attachment.ts
2992
+ var log20 = createLogger("tool:add-attachment");
2993
+ var addAttachmentTool = {
2994
+ name: TOOL_NAMES.ADD_ATTACHMENT,
2995
+ description: `Uploads an attachment (image, document, etc.) to a Jira issue.
2996
+
2997
+ **EFFICIENT METHOD (recommended for large files):**
2998
+ - fileUrl: Provide URL to remote file - MINIMAL tokens (~60 tokens)
2999
+ Example: Upload to Dropbox/S3/imgur first, then provide URL
3000
+
3001
+ **DIRECT METHOD (for small files):**
3002
+ - content: Base64 encoded content - WARNING: HIGH token cost (~330,000 tokens for 1MB file)
3003
+ Only suitable for small files (< 500 KB)
3004
+
3005
+ Returns attachment metadata including ID and download URL. To reference the image in a comment or description, use wiki markup: !filename.png! or !filename.png|thumbnail!`,
3006
+ inputSchema: {
3007
+ type: "object",
3008
+ properties: {
3009
+ issueKey: {
3010
+ type: "string",
3011
+ description: "Issue key to add attachment to (e.g., PROJECT-123)"
3012
+ },
3013
+ filename: {
3014
+ type: "string",
3015
+ description: "Name of the file to attach (e.g., screenshot.png, document.pdf)",
3016
+ minLength: 1
3017
+ },
3018
+ fileUrl: {
3019
+ type: "string",
3020
+ description: "URL to download file from (efficient for remote files, ~60 tokens). Upload large files to Dropbox/S3/imgur first, then use URL.",
3021
+ format: "uri"
3022
+ },
3023
+ content: {
3024
+ type: "string",
3025
+ description: "Base64-encoded or plain text content. WARNING: HIGH token cost (~330k tokens for 1MB file). Use fileUrl for large files or upload to cloud storage first."
3026
+ },
3027
+ isBase64: {
3028
+ type: "boolean",
3029
+ description: "Whether content is base64-encoded (default: true). Set to false for plain text files.",
3030
+ default: true
3031
+ }
3032
+ },
3033
+ required: ["issueKey", "filename"]
3034
+ }
3035
+ };
3036
+ async function handleAddAttachment(input) {
3037
+ try {
3038
+ const validated = validateInput(AddAttachmentInputSchema, input);
3039
+ const { issueKey, filename, fileUrl, content, isBase64 } = validated;
3040
+ log20.info(`Adding attachment "${filename}" to issue ${issueKey}...`);
3041
+ let attachments;
3042
+ if (fileUrl) {
3043
+ log20.info("Using URL method (efficient - ~60 tokens)");
3044
+ attachments = await addAttachmentFromUrl(issueKey, fileUrl, filename);
3045
+ } else if (content) {
3046
+ if (content.length > ATTACHMENT_CONFIG.MAX_BASE64_SIZE) {
3047
+ log20.warn(
3048
+ `Large base64 content detected (${content.length} chars = ~${Math.round(content.length / 4)} tokens). Consider uploading to cloud storage and using fileUrl instead.`
3049
+ );
3050
+ }
3051
+ log20.info("Using base64 content method (HIGH token cost for large files)");
3052
+ attachments = await addAttachment(issueKey, filename, content, isBase64);
3053
+ } else {
3054
+ throw new Error("No file source provided (fileUrl or content required)");
3055
+ }
3056
+ if (!attachments || attachments.length === 0) {
3057
+ throw new Error("Failed to upload attachment - no attachment returned from Jira");
3058
+ }
3059
+ log20.info(`Added attachment "${filename}" to ${issueKey}`);
3060
+ return formatAttachmentResponse(attachments[0], issueKey);
3061
+ } catch (error) {
3062
+ log20.error("Error in handleAddAttachment:", error);
3063
+ return handleError(error);
3064
+ }
3065
+ }
3066
+
3067
+ // src/tools/get-attachments.ts
3068
+ var log21 = createLogger("tool:get-attachments");
3069
+ var getAttachmentsTool = {
3070
+ name: TOOL_NAMES.GET_ATTACHMENTS,
3071
+ description: "Lists all attachments on a Jira issue. Returns attachment metadata including filename, size, MIME type, author, and download URL.",
3072
+ inputSchema: {
3073
+ type: "object",
3074
+ properties: {
3075
+ issueKey: {
3076
+ type: "string",
3077
+ description: "Issue key to get attachments for (e.g., PROJECT-123)"
3078
+ }
3079
+ },
3080
+ required: ["issueKey"]
3081
+ }
3082
+ };
3083
+ async function handleGetAttachments(input) {
3084
+ try {
3085
+ const validated = validateInput(GetAttachmentsInputSchema, input);
3086
+ log21.info(`Getting attachments for issue ${validated.issueKey}...`);
3087
+ const attachments = await getAttachments(validated.issueKey);
3088
+ log21.info(`Found ${attachments.length} attachment(s) for ${validated.issueKey}`);
3089
+ return formatAttachmentsListResponse(attachments, validated.issueKey);
3090
+ } catch (error) {
3091
+ log21.error("Error in handleGetAttachments:", error);
3092
+ return handleError(error);
3093
+ }
3094
+ }
3095
+
3096
+ // src/tools/delete-attachment.ts
3097
+ var log22 = createLogger("tool:delete-attachment");
3098
+ var deleteAttachmentTool = {
3099
+ name: TOOL_NAMES.DELETE_ATTACHMENT,
3100
+ description: "Deletes an attachment from Jira by its attachment ID. Use jira_get_attachments to find attachment IDs.",
3101
+ inputSchema: {
3102
+ type: "object",
3103
+ properties: {
3104
+ attachmentId: {
3105
+ type: "string",
3106
+ description: "ID of the attachment to delete (can be found using jira_get_attachments)",
3107
+ minLength: 1
3108
+ }
3109
+ },
3110
+ required: ["attachmentId"]
3111
+ }
3112
+ };
3113
+ async function handleDeleteAttachment(input) {
3114
+ try {
3115
+ const validated = validateInput(DeleteAttachmentInputSchema, input);
3116
+ log22.info(`Deleting attachment ${validated.attachmentId}...`);
3117
+ await deleteAttachment(validated.attachmentId);
3118
+ log22.info(`Deleted attachment ${validated.attachmentId}`);
3119
+ return {
3120
+ content: [
3121
+ {
3122
+ type: "text",
3123
+ text: `Successfully deleted attachment ${validated.attachmentId}`
3124
+ }
3125
+ ]
3126
+ };
3127
+ } catch (error) {
3128
+ log22.error("Error in handleDeleteAttachment:", error);
3129
+ return handleError(error);
3130
+ }
3131
+ }
3132
+
2625
3133
  // src/index.ts
2626
3134
  if (process.env.NODE_ENV !== "production") {
2627
3135
  try {
@@ -2630,7 +3138,7 @@ if (process.env.NODE_ENV !== "production") {
2630
3138
  } catch {
2631
3139
  }
2632
3140
  }
2633
- var log19 = createLogger("server");
3141
+ var log23 = createLogger("server");
2634
3142
  var toolHandlers = /* @__PURE__ */ new Map([
2635
3143
  [TOOL_NAMES.GET_VISIBLE_PROJECTS, handleGetVisibleProjects],
2636
3144
  [TOOL_NAMES.GET_ISSUE, handleGetIssue],
@@ -2647,7 +3155,11 @@ var toolHandlers = /* @__PURE__ */ new Map([
2647
3155
  [TOOL_NAMES.CREATE_SUBTASK, handleCreateSubtask],
2648
3156
  [TOOL_NAMES.GET_CREATE_META, handleGetCreateMeta],
2649
3157
  [TOOL_NAMES.GET_CUSTOM_FIELDS, handleGetCustomFields],
2650
- [TOOL_NAMES.CREATE_ISSUE_LINK, handleCreateIssueLink]
3158
+ [TOOL_NAMES.CREATE_ISSUE_LINK, handleCreateIssueLink],
3159
+ [TOOL_NAMES.GET_COMMENTS, handleGetComments],
3160
+ [TOOL_NAMES.ADD_ATTACHMENT, handleAddAttachment],
3161
+ [TOOL_NAMES.GET_ATTACHMENTS, handleGetAttachments],
3162
+ [TOOL_NAMES.DELETE_ATTACHMENT, handleDeleteAttachment]
2651
3163
  ]);
2652
3164
  var allTools = [
2653
3165
  getVisibleProjectsTool,
@@ -2665,7 +3177,11 @@ var allTools = [
2665
3177
  createSubtaskTool,
2666
3178
  getCreateMetaTool,
2667
3179
  getCustomFieldsTool,
2668
- createIssueLinkTool
3180
+ createIssueLinkTool,
3181
+ getCommentsTool,
3182
+ addAttachmentTool,
3183
+ getAttachmentsTool,
3184
+ deleteAttachmentTool
2669
3185
  ];
2670
3186
  async function main() {
2671
3187
  const isDryRun = (process.env.DRY_RUN || "").toLowerCase() === "1" || (process.env.DRY_RUN || "").toLowerCase() === "true";
@@ -2736,7 +3252,7 @@ async function main() {
2736
3252
  console.error("\u{1F680} Jira MCP server running on stdio");
2737
3253
  }
2738
3254
  main().catch((error) => {
2739
- log19.error("Fatal error:", error);
3255
+ log23.error("Fatal error:", error);
2740
3256
  process.exit(1);
2741
3257
  });
2742
3258
  //# sourceMappingURL=index.js.map