mcp-jira-stdio 1.11.1 → 1.12.1

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
@@ -185,7 +185,7 @@ Restart Claude Desktop after adding the configuration.
185
185
  - `jira_get_issue`: Retrieve issue details by key (supports optional fields/expand).
186
186
  - `jira_search_issues`: Search for Jira issues using JQL with pagination and fields.
187
187
  - `jira_create_issue`: Create a new issue in a project (type, priority, assignee, labels, components).
188
- - `jira_update_issue`: Update an existing issue (summary, description, priority, assignee, labels, components).
188
+ - `jira_update_issue`: Update an existing issue (summary, description, priority, assignee, labels, components, fix versions).
189
189
  - `jira_create_subtask`: Create a subtask under a parent issue (auto-detects subtask type).
190
190
 
191
191
  ### Comments
package/dist/index.js CHANGED
@@ -440,7 +440,7 @@ var require_package2 = __commonJS({
440
440
  "package.json"(exports$1, module) {
441
441
  module.exports = {
442
442
  name: "mcp-jira-stdio",
443
- version: "1.11.1",
443
+ version: "1.12.1",
444
444
  mcpName: "io.github.freema/mcp-jira-stdio",
445
445
  description: "Model Context Protocol (MCP) server for Jira API integration",
446
446
  author: "graslt",
@@ -546,6 +546,8 @@ var JIRA_CONFIG = {
546
546
  var ERROR_MESSAGES = {
547
547
  AUTH_REQUIRED: "Jira authentication required. Set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN environment variables.",
548
548
  INVALID_CREDENTIALS: "Invalid Jira credentials. Please check your email and API token.",
549
+ PROJECT_NOT_FOUND: "Project not found or you don't have permission to access it.",
550
+ ISSUE_NOT_FOUND: "Issue not found or you don't have permission to access it.",
549
551
  RATE_LIMIT_EXCEEDED: "Jira API rate limit exceeded. Please wait and try again.",
550
552
  NETWORK_ERROR: "Network error occurred while connecting to Jira.",
551
553
  VALIDATION_ERROR: "Input validation failed."
@@ -597,23 +599,75 @@ function getLevel() {
597
599
  return env;
598
600
  return "info";
599
601
  }
602
+ var REDACTED = "[REDACTED]";
603
+ var SECRET_KEYS = /^(auth|authorization|proxy-authorization|password|apitoken|api_token|token|cookie|set-cookie)$/i;
604
+ function describeError(err) {
605
+ const e = err;
606
+ const out = {
607
+ name: e?.name ?? "Error",
608
+ message: e?.message ?? String(err)
609
+ };
610
+ if (e?.code) out.code = e.code;
611
+ const status = e?.response?.status ?? e?.status;
612
+ if (status) out.status = status;
613
+ const config = e?.config;
614
+ if (config?.method) out.method = String(config.method).toUpperCase();
615
+ if (config?.url) out.url = `${config.baseURL ?? ""}${config.url}`;
616
+ const data = e?.response?.data;
617
+ if (Array.isArray(data?.errorMessages) && data.errorMessages.length > 0) {
618
+ out.errorMessages = data.errorMessages;
619
+ }
620
+ if (data?.errors && typeof data.errors === "object" && Object.keys(data.errors).length > 0) {
621
+ out.errors = data.errors;
622
+ }
623
+ return out;
624
+ }
625
+ function redactDeep(value, seen, depth) {
626
+ if (value instanceof Error) return describeError(value);
627
+ if (value === null || typeof value !== "object") return value;
628
+ if (seen.has(value) || depth > 8) return "[Circular]";
629
+ seen.add(value);
630
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, seen, depth + 1));
631
+ const out = {};
632
+ for (const [key, v] of Object.entries(value)) {
633
+ out[key] = SECRET_KEYS.test(key) ? REDACTED : redactDeep(v, seen, depth + 1);
634
+ }
635
+ return out;
636
+ }
637
+ function scrubSecrets(text) {
638
+ let out = text.replace(/(\/\/)[^/@\s]+@/g, `$1${REDACTED}@`).replace(/\b(Basic|Bearer)\s+[A-Za-z0-9+/=._~-]{16,}/g, `$1 ${REDACTED}`);
639
+ const token = process.env.JIRA_API_TOKEN;
640
+ if (token && token.length >= 8) {
641
+ out = out.split(token).join(REDACTED);
642
+ }
643
+ return out;
644
+ }
645
+ function formatLogArg(arg) {
646
+ if (typeof arg === "string") return scrubSecrets(arg);
647
+ let text;
648
+ try {
649
+ text = JSON.stringify(redactDeep(arg, /* @__PURE__ */ new WeakSet(), 0)) ?? String(arg);
650
+ } catch {
651
+ text = String(arg);
652
+ }
653
+ return scrubSecrets(text);
654
+ }
600
655
  function createLogger(scope) {
601
656
  const level = getLevel();
602
657
  const min = levels[level];
603
- const fmt = (msg) => typeof msg === "string" ? msg : JSON.stringify(msg);
604
658
  const prefix = (lvl) => `[${lvl.toUpperCase()}] ${scope}:`;
605
659
  return {
606
660
  debug: (...args) => {
607
- if (min <= levels.debug) console.error(prefix("debug"), ...args.map(fmt));
661
+ if (min <= levels.debug) console.error(prefix("debug"), ...args.map(formatLogArg));
608
662
  },
609
663
  info: (...args) => {
610
- if (min <= levels.info) console.error(prefix("info"), ...args.map(fmt));
664
+ if (min <= levels.info) console.error(prefix("info"), ...args.map(formatLogArg));
611
665
  },
612
666
  warn: (...args) => {
613
- if (min <= levels.warn) console.error(prefix("warn"), ...args.map(fmt));
667
+ if (min <= levels.warn) console.error(prefix("warn"), ...args.map(formatLogArg));
614
668
  },
615
669
  error: (...args) => {
616
- if (min <= levels.error) console.error(prefix("error"), ...args.map(fmt));
670
+ if (min <= levels.error) console.error(prefix("error"), ...args.map(formatLogArg));
617
671
  }
618
672
  };
619
673
  }
@@ -886,6 +940,7 @@ var UpdateIssueInputSchema = z.object({
886
940
  assignee: z.string().optional().describe("New assignee account ID"),
887
941
  labels: z.array(z.string()).optional().describe("New labels (replaces existing)"),
888
942
  components: z.array(z.string()).optional().describe("New components (replaces existing)"),
943
+ fixVersions: z.array(z.string()).optional().describe("New fix version names (replaces existing fix versions)"),
889
944
  parent: z.string().optional().describe(
890
945
  "New parent issue key (e.g., PROJECT-100). Set to empty string to remove the parent."
891
946
  ),
@@ -933,7 +988,7 @@ var GetProjectInfoInputSchema = z.object({
933
988
  expand: z.array(z.string()).optional().describe("Additional project details to include")
934
989
  });
935
990
  var CreateSubtaskInputSchema = z.object({
936
- parentIssueKey: z.string().describe("Parent issue key"),
991
+ parentIssueKey: z.string().describe("Parent issue key").refine((v) => isValidIssueKey(v), "Invalid issue key format"),
937
992
  summary: z.string().min(1).describe("Subtask summary/title"),
938
993
  description: z.union([z.string(), z.any()]).optional().describe("Subtask description. Accepts plain text or ADF object."),
939
994
  priority: z.string().optional().describe("Subtask priority"),
@@ -989,7 +1044,7 @@ var GetAttachmentsInputSchema = z.object({
989
1044
  issueKey: z.string().describe("Issue key to get attachments for (e.g., PROJECT-123)").refine((v) => isValidIssueKey(v), "Invalid issue key format")
990
1045
  });
991
1046
  var DeleteAttachmentInputSchema = z.object({
992
- attachmentId: z.string().min(1).describe("ID of the attachment to delete")
1047
+ attachmentId: z.string().regex(/^\d+$/, "Attachment ID must be numeric").describe("ID of the attachment to delete")
993
1048
  });
994
1049
  var GetTransitionsInputSchema = z.object({
995
1050
  issueKey: z.string().describe("Issue key to get available transitions for (e.g., PROJECT-123)").refine((v) => isValidIssueKey(v), "Invalid issue key format")
@@ -1022,6 +1077,12 @@ var GetIssueGraphInputSchema = z.object({
1022
1077
  ),
1023
1078
  direction: z.enum(["all", "inward", "outward"]).default("all").describe('Which link directions to follow: "all", "inward", or "outward" (default: "all")')
1024
1079
  });
1080
+ function pathSegment(value) {
1081
+ if (typeof value !== "string" || value === "" || value === "." || value === "..") {
1082
+ throw new Error(`Invalid identifier: ${JSON.stringify(value)}`);
1083
+ }
1084
+ return encodeURIComponent(value);
1085
+ }
1025
1086
  function ensureAdfDescription(desc, format = "markdown") {
1026
1087
  if (!desc) return desc;
1027
1088
  if (typeof desc === "object") return desc;
@@ -1197,7 +1258,7 @@ async function getIssue(issueKey, options = {}) {
1197
1258
  }
1198
1259
  const config = {
1199
1260
  method: "GET",
1200
- url: `/issue/${issueKey}`,
1261
+ url: `/issue/${pathSegment(issueKey)}`,
1201
1262
  params
1202
1263
  };
1203
1264
  return await makeJiraRequest(config);
@@ -1287,12 +1348,15 @@ async function updateIssue(issueKey, updates) {
1287
1348
  if (updates.components !== void 0) {
1288
1349
  fields.components = updates.components.map((name) => ({ name }));
1289
1350
  }
1351
+ if (updates.fixVersions !== void 0) {
1352
+ fields.fixVersions = updates.fixVersions.map((name) => ({ name }));
1353
+ }
1290
1354
  if (updates.parent !== void 0) {
1291
1355
  fields.parent = updates.parent ? { key: updates.parent } : null;
1292
1356
  }
1293
1357
  const config = {
1294
1358
  method: "PUT",
1295
- url: `/issue/${issueKey}`,
1359
+ url: `/issue/${pathSegment(issueKey)}`,
1296
1360
  data: { fields }
1297
1361
  };
1298
1362
  await makeJiraRequest(config);
@@ -1311,7 +1375,7 @@ async function getMyIssues(options = {}) {
1311
1375
  async function getIssueTypes(projectKey) {
1312
1376
  let url = "/issuetype";
1313
1377
  if (projectKey) {
1314
- url = `/project/${projectKey}/issuetype`;
1378
+ url = `/project/${pathSegment(projectKey)}/issuetype`;
1315
1379
  }
1316
1380
  const config = {
1317
1381
  method: "GET",
@@ -1353,9 +1417,9 @@ async function getPriorities() {
1353
1417
  async function getStatuses(options = {}) {
1354
1418
  let url = "/status";
1355
1419
  if (options.projectKey && options.issueTypeId) {
1356
- url = `/project/${options.projectKey}/statuses`;
1420
+ url = `/project/${pathSegment(options.projectKey)}/statuses`;
1357
1421
  } else if (options.projectKey) {
1358
- url = `/project/${options.projectKey}/statuses`;
1422
+ url = `/project/${pathSegment(options.projectKey)}/statuses`;
1359
1423
  }
1360
1424
  const config = {
1361
1425
  method: "GET",
@@ -1383,7 +1447,7 @@ async function addComment(issueKey, body, visibility, format) {
1383
1447
  }
1384
1448
  const config = {
1385
1449
  method: "POST",
1386
- url: `/issue/${issueKey}/comment`,
1450
+ url: `/issue/${pathSegment(issueKey)}/comment`,
1387
1451
  data
1388
1452
  };
1389
1453
  return await makeJiraRequest(config);
@@ -1401,7 +1465,7 @@ async function getComments(issueKey, options = {}) {
1401
1465
  }
1402
1466
  const config = {
1403
1467
  method: "GET",
1404
- url: `/issue/${issueKey}/comment`,
1468
+ url: `/issue/${pathSegment(issueKey)}/comment`,
1405
1469
  params
1406
1470
  };
1407
1471
  return await makeJiraRequest(config);
@@ -1413,7 +1477,7 @@ async function getProjectDetails(projectKey, expand) {
1413
1477
  }
1414
1478
  const config = {
1415
1479
  method: "GET",
1416
- url: `/project/${projectKey}`,
1480
+ url: `/project/${pathSegment(projectKey)}`,
1417
1481
  params
1418
1482
  };
1419
1483
  return await makeJiraRequest(config);
@@ -1529,7 +1593,10 @@ async function addAttachment(issueKey, filename, content, isBase64 = true) {
1529
1593
  filename,
1530
1594
  contentType: "application/octet-stream"
1531
1595
  });
1532
- return await makeMultipartRequest(`/issue/${issueKey}/attachments`, formData);
1596
+ return await makeMultipartRequest(
1597
+ `/issue/${pathSegment(issueKey)}/attachments`,
1598
+ formData
1599
+ );
1533
1600
  }
1534
1601
  async function addAttachmentFromUrl(issueKey, fileUrl, filename) {
1535
1602
  const axios2 = (await import('axios')).default;
@@ -1560,7 +1627,10 @@ async function addAttachmentFromUrl(issueKey, fileUrl, filename) {
1560
1627
  filename,
1561
1628
  contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : "application/octet-stream"
1562
1629
  });
1563
- return await makeMultipartRequest(`/issue/${issueKey}/attachments`, formData);
1630
+ return await makeMultipartRequest(
1631
+ `/issue/${pathSegment(issueKey)}/attachments`,
1632
+ formData
1633
+ );
1564
1634
  }
1565
1635
  async function getAttachments(issueKey) {
1566
1636
  const issue = await getIssue(issueKey, { fields: ["attachment"] });
@@ -1570,14 +1640,14 @@ async function getAttachments(issueKey) {
1570
1640
  async function deleteAttachment(attachmentId) {
1571
1641
  const config = {
1572
1642
  method: "DELETE",
1573
- url: `/attachment/${attachmentId}`
1643
+ url: `/attachment/${pathSegment(attachmentId)}`
1574
1644
  };
1575
1645
  await makeJiraRequest(config);
1576
1646
  }
1577
1647
  async function getTransitions(issueKey) {
1578
1648
  const config = {
1579
1649
  method: "GET",
1580
- url: `/issue/${issueKey}/transitions`
1650
+ url: `/issue/${pathSegment(issueKey)}/transitions`
1581
1651
  };
1582
1652
  const response = await makeJiraRequest(config);
1583
1653
  return response.transitions;
@@ -1605,7 +1675,7 @@ async function transitionIssue(issueKey, transitionId, options = {}) {
1605
1675
  }
1606
1676
  const config = {
1607
1677
  method: "POST",
1608
- url: `/issue/${issueKey}/transitions`,
1678
+ url: `/issue/${pathSegment(issueKey)}/transitions`,
1609
1679
  data
1610
1680
  };
1611
1681
  await makeJiraRequest(config);
@@ -1687,6 +1757,8 @@ function formatIssueResponse(issue) {
1687
1757
  const labelsText = labelsArray.length > 0 ? labelsArray.join(", ") : "None";
1688
1758
  const componentsArray = Array.isArray(fields?.components) ? fields.components : [];
1689
1759
  const componentsText = componentsArray.length > 0 ? componentsArray.map((c) => c.name).join(", ") : "None";
1760
+ const fixVersionsArray = Array.isArray(fields?.fixVersions) ? fields.fixVersions : [];
1761
+ const fixVersionsText = fixVersionsArray.length > 0 ? fixVersionsArray.map((v) => v.name).join(", ") : "None";
1690
1762
  let description = fields?.description;
1691
1763
  if (description && typeof description === "object") {
1692
1764
  const parsed = adfToPlainText(description);
@@ -1712,7 +1784,8 @@ function formatIssueResponse(issue) {
1712
1784
  **Project:** ${fields?.project?.name || "Unknown"} (${fields?.project?.key || "N/A"})
1713
1785
  **Issue Type:** ${fields?.issuetype?.name || "Unknown"}
1714
1786
  **Labels:** ${labelsText}
1715
- **Components:** ${componentsText}${parentText}
1787
+ **Components:** ${componentsText}
1788
+ **Fix Versions:** ${fixVersionsText}${parentText}
1716
1789
  **Created:** ${createdText}
1717
1790
  **Updated:** ${updatedText}
1718
1791
 
@@ -2259,6 +2332,10 @@ async function handleGetIssue(input) {
2259
2332
  const validated = validateInput(GetIssueInputSchema, input);
2260
2333
  const trimmedInput = (validated.issueKey || "").trim();
2261
2334
  const key = extractIssueKey(trimmedInput) || trimmedInput;
2335
+ if (!/^[A-Za-z][A-Za-z0-9_]*-\d+$/.test(key) && !/^\d+$/.test(key)) {
2336
+ throw new Error(`${ERROR_MESSAGES.VALIDATION_ERROR}
2337
+ issueKey: Invalid issue key format`);
2338
+ }
2262
2339
  log4.info(`Getting issue ${key}...`);
2263
2340
  const getParams = {};
2264
2341
  if (validated.expand !== void 0) getParams.expand = validated.expand;
@@ -2460,7 +2537,7 @@ async function handleCreateIssue(input) {
2460
2537
  // src/tools/update-issue.ts
2461
2538
  var log12 = createLogger("tool:update-issue");
2462
2539
  var updateIssueTool = {
2463
- description: 'Updates an existing Jira issue by its key. Supports updating summary, description, priority, assignee, labels, and components. Description format is controlled by the "format" parameter (default: markdown). Only specified fields will be updated.'};
2540
+ description: 'Updates an existing Jira issue by its key. Supports updating summary, description, priority, assignee, labels, components, and fix versions. Description format is controlled by the "format" parameter (default: markdown). Only specified fields will be updated.'};
2464
2541
  async function handleUpdateIssue(input) {
2465
2542
  try {
2466
2543
  const validated = validateInput(UpdateIssueInputSchema, input);
@@ -2472,6 +2549,7 @@ async function handleUpdateIssue(input) {
2472
2549
  if (validated.assignee !== void 0) updateParams.assignee = validated.assignee;
2473
2550
  if (validated.labels !== void 0) updateParams.labels = validated.labels;
2474
2551
  if (validated.components !== void 0) updateParams.components = validated.components;
2552
+ if (validated.fixVersions !== void 0) updateParams.fixVersions = validated.fixVersions;
2475
2553
  if (validated.parent !== void 0) updateParams.parent = validated.parent;
2476
2554
  if (validated.format !== void 0) updateParams.format = validated.format;
2477
2555
  await updateIssue(validated.issueKey, updateParams);