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 +1 -1
- package/dist/index.js +101 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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().
|
|
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(
|
|
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(
|
|
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}
|
|
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
|
|
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);
|