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 +12 -0
- package/dist/index.js +522 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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.
|
|
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
|
|
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
|
-
|
|
3255
|
+
log23.error("Fatal error:", error);
|
|
2740
3256
|
process.exit(1);
|
|
2741
3257
|
});
|
|
2742
3258
|
//# sourceMappingURL=index.js.map
|