mcp-jira-stdio 1.12.0 → 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/dist/index.js +91 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.12.
|
|
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
|
}
|
|
@@ -934,7 +988,7 @@ var GetProjectInfoInputSchema = z.object({
|
|
|
934
988
|
expand: z.array(z.string()).optional().describe("Additional project details to include")
|
|
935
989
|
});
|
|
936
990
|
var CreateSubtaskInputSchema = z.object({
|
|
937
|
-
parentIssueKey: z.string().describe("Parent issue key"),
|
|
991
|
+
parentIssueKey: z.string().describe("Parent issue key").refine((v) => isValidIssueKey(v), "Invalid issue key format"),
|
|
938
992
|
summary: z.string().min(1).describe("Subtask summary/title"),
|
|
939
993
|
description: z.union([z.string(), z.any()]).optional().describe("Subtask description. Accepts plain text or ADF object."),
|
|
940
994
|
priority: z.string().optional().describe("Subtask priority"),
|
|
@@ -990,7 +1044,7 @@ var GetAttachmentsInputSchema = z.object({
|
|
|
990
1044
|
issueKey: z.string().describe("Issue key to get attachments for (e.g., PROJECT-123)").refine((v) => isValidIssueKey(v), "Invalid issue key format")
|
|
991
1045
|
});
|
|
992
1046
|
var DeleteAttachmentInputSchema = z.object({
|
|
993
|
-
attachmentId: z.string().
|
|
1047
|
+
attachmentId: z.string().regex(/^\d+$/, "Attachment ID must be numeric").describe("ID of the attachment to delete")
|
|
994
1048
|
});
|
|
995
1049
|
var GetTransitionsInputSchema = z.object({
|
|
996
1050
|
issueKey: z.string().describe("Issue key to get available transitions for (e.g., PROJECT-123)").refine((v) => isValidIssueKey(v), "Invalid issue key format")
|
|
@@ -1023,6 +1077,12 @@ var GetIssueGraphInputSchema = z.object({
|
|
|
1023
1077
|
),
|
|
1024
1078
|
direction: z.enum(["all", "inward", "outward"]).default("all").describe('Which link directions to follow: "all", "inward", or "outward" (default: "all")')
|
|
1025
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
|
+
}
|
|
1026
1086
|
function ensureAdfDescription(desc, format = "markdown") {
|
|
1027
1087
|
if (!desc) return desc;
|
|
1028
1088
|
if (typeof desc === "object") return desc;
|
|
@@ -1198,7 +1258,7 @@ async function getIssue(issueKey, options = {}) {
|
|
|
1198
1258
|
}
|
|
1199
1259
|
const config = {
|
|
1200
1260
|
method: "GET",
|
|
1201
|
-
url: `/issue/${issueKey}`,
|
|
1261
|
+
url: `/issue/${pathSegment(issueKey)}`,
|
|
1202
1262
|
params
|
|
1203
1263
|
};
|
|
1204
1264
|
return await makeJiraRequest(config);
|
|
@@ -1296,7 +1356,7 @@ async function updateIssue(issueKey, updates) {
|
|
|
1296
1356
|
}
|
|
1297
1357
|
const config = {
|
|
1298
1358
|
method: "PUT",
|
|
1299
|
-
url: `/issue/${issueKey}`,
|
|
1359
|
+
url: `/issue/${pathSegment(issueKey)}`,
|
|
1300
1360
|
data: { fields }
|
|
1301
1361
|
};
|
|
1302
1362
|
await makeJiraRequest(config);
|
|
@@ -1315,7 +1375,7 @@ async function getMyIssues(options = {}) {
|
|
|
1315
1375
|
async function getIssueTypes(projectKey) {
|
|
1316
1376
|
let url = "/issuetype";
|
|
1317
1377
|
if (projectKey) {
|
|
1318
|
-
url = `/project/${projectKey}/issuetype`;
|
|
1378
|
+
url = `/project/${pathSegment(projectKey)}/issuetype`;
|
|
1319
1379
|
}
|
|
1320
1380
|
const config = {
|
|
1321
1381
|
method: "GET",
|
|
@@ -1357,9 +1417,9 @@ async function getPriorities() {
|
|
|
1357
1417
|
async function getStatuses(options = {}) {
|
|
1358
1418
|
let url = "/status";
|
|
1359
1419
|
if (options.projectKey && options.issueTypeId) {
|
|
1360
|
-
url = `/project/${options.projectKey}/statuses`;
|
|
1420
|
+
url = `/project/${pathSegment(options.projectKey)}/statuses`;
|
|
1361
1421
|
} else if (options.projectKey) {
|
|
1362
|
-
url = `/project/${options.projectKey}/statuses`;
|
|
1422
|
+
url = `/project/${pathSegment(options.projectKey)}/statuses`;
|
|
1363
1423
|
}
|
|
1364
1424
|
const config = {
|
|
1365
1425
|
method: "GET",
|
|
@@ -1387,7 +1447,7 @@ async function addComment(issueKey, body, visibility, format) {
|
|
|
1387
1447
|
}
|
|
1388
1448
|
const config = {
|
|
1389
1449
|
method: "POST",
|
|
1390
|
-
url: `/issue/${issueKey}/comment`,
|
|
1450
|
+
url: `/issue/${pathSegment(issueKey)}/comment`,
|
|
1391
1451
|
data
|
|
1392
1452
|
};
|
|
1393
1453
|
return await makeJiraRequest(config);
|
|
@@ -1405,7 +1465,7 @@ async function getComments(issueKey, options = {}) {
|
|
|
1405
1465
|
}
|
|
1406
1466
|
const config = {
|
|
1407
1467
|
method: "GET",
|
|
1408
|
-
url: `/issue/${issueKey}/comment`,
|
|
1468
|
+
url: `/issue/${pathSegment(issueKey)}/comment`,
|
|
1409
1469
|
params
|
|
1410
1470
|
};
|
|
1411
1471
|
return await makeJiraRequest(config);
|
|
@@ -1417,7 +1477,7 @@ async function getProjectDetails(projectKey, expand) {
|
|
|
1417
1477
|
}
|
|
1418
1478
|
const config = {
|
|
1419
1479
|
method: "GET",
|
|
1420
|
-
url: `/project/${projectKey}`,
|
|
1480
|
+
url: `/project/${pathSegment(projectKey)}`,
|
|
1421
1481
|
params
|
|
1422
1482
|
};
|
|
1423
1483
|
return await makeJiraRequest(config);
|
|
@@ -1533,7 +1593,10 @@ async function addAttachment(issueKey, filename, content, isBase64 = true) {
|
|
|
1533
1593
|
filename,
|
|
1534
1594
|
contentType: "application/octet-stream"
|
|
1535
1595
|
});
|
|
1536
|
-
return await makeMultipartRequest(
|
|
1596
|
+
return await makeMultipartRequest(
|
|
1597
|
+
`/issue/${pathSegment(issueKey)}/attachments`,
|
|
1598
|
+
formData
|
|
1599
|
+
);
|
|
1537
1600
|
}
|
|
1538
1601
|
async function addAttachmentFromUrl(issueKey, fileUrl, filename) {
|
|
1539
1602
|
const axios2 = (await import('axios')).default;
|
|
@@ -1564,7 +1627,10 @@ async function addAttachmentFromUrl(issueKey, fileUrl, filename) {
|
|
|
1564
1627
|
filename,
|
|
1565
1628
|
contentType: typeof response.headers["content-type"] === "string" ? response.headers["content-type"] : "application/octet-stream"
|
|
1566
1629
|
});
|
|
1567
|
-
return await makeMultipartRequest(
|
|
1630
|
+
return await makeMultipartRequest(
|
|
1631
|
+
`/issue/${pathSegment(issueKey)}/attachments`,
|
|
1632
|
+
formData
|
|
1633
|
+
);
|
|
1568
1634
|
}
|
|
1569
1635
|
async function getAttachments(issueKey) {
|
|
1570
1636
|
const issue = await getIssue(issueKey, { fields: ["attachment"] });
|
|
@@ -1574,14 +1640,14 @@ async function getAttachments(issueKey) {
|
|
|
1574
1640
|
async function deleteAttachment(attachmentId) {
|
|
1575
1641
|
const config = {
|
|
1576
1642
|
method: "DELETE",
|
|
1577
|
-
url: `/attachment/${attachmentId}`
|
|
1643
|
+
url: `/attachment/${pathSegment(attachmentId)}`
|
|
1578
1644
|
};
|
|
1579
1645
|
await makeJiraRequest(config);
|
|
1580
1646
|
}
|
|
1581
1647
|
async function getTransitions(issueKey) {
|
|
1582
1648
|
const config = {
|
|
1583
1649
|
method: "GET",
|
|
1584
|
-
url: `/issue/${issueKey}/transitions`
|
|
1650
|
+
url: `/issue/${pathSegment(issueKey)}/transitions`
|
|
1585
1651
|
};
|
|
1586
1652
|
const response = await makeJiraRequest(config);
|
|
1587
1653
|
return response.transitions;
|
|
@@ -1609,7 +1675,7 @@ async function transitionIssue(issueKey, transitionId, options = {}) {
|
|
|
1609
1675
|
}
|
|
1610
1676
|
const config = {
|
|
1611
1677
|
method: "POST",
|
|
1612
|
-
url: `/issue/${issueKey}/transitions`,
|
|
1678
|
+
url: `/issue/${pathSegment(issueKey)}/transitions`,
|
|
1613
1679
|
data
|
|
1614
1680
|
};
|
|
1615
1681
|
await makeJiraRequest(config);
|
|
@@ -2266,6 +2332,10 @@ async function handleGetIssue(input) {
|
|
|
2266
2332
|
const validated = validateInput(GetIssueInputSchema, input);
|
|
2267
2333
|
const trimmedInput = (validated.issueKey || "").trim();
|
|
2268
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
|
+
}
|
|
2269
2339
|
log4.info(`Getting issue ${key}...`);
|
|
2270
2340
|
const getParams = {};
|
|
2271
2341
|
if (validated.expand !== void 0) getParams.expand = validated.expand;
|