mcp-jira-stdio 1.7.1 → 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/dist/index.js +402 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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",
|
|
@@ -565,7 +574,10 @@ var TOOL_NAMES = {
|
|
|
565
574
|
GET_CREATE_META: "jira_get_create_meta",
|
|
566
575
|
GET_CUSTOM_FIELDS: "jira_get_custom_fields",
|
|
567
576
|
CREATE_ISSUE_LINK: "jira_create_issue_link",
|
|
568
|
-
GET_COMMENTS: "jira_get_comments"
|
|
577
|
+
GET_COMMENTS: "jira_get_comments",
|
|
578
|
+
ADD_ATTACHMENT: "jira_add_attachment",
|
|
579
|
+
GET_ATTACHMENTS: "jira_get_attachments",
|
|
580
|
+
DELETE_ATTACHMENT: "jira_delete_attachment"
|
|
569
581
|
};
|
|
570
582
|
|
|
571
583
|
// src/utils/logger.ts
|
|
@@ -605,6 +617,7 @@ function createLogger(scope) {
|
|
|
605
617
|
|
|
606
618
|
// src/utils/jira-auth.ts
|
|
607
619
|
var jiraClient = null;
|
|
620
|
+
var jiraMultipartClient = null;
|
|
608
621
|
var log = createLogger("jira-auth");
|
|
609
622
|
function validateAuth() {
|
|
610
623
|
const baseUrl = process.env.JIRA_BASE_URL;
|
|
@@ -707,6 +720,96 @@ async function makeJiraRequest(config, retryConfig = {
|
|
|
707
720
|
}
|
|
708
721
|
throw lastError;
|
|
709
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
|
+
}
|
|
710
813
|
function validateInput(schema, input) {
|
|
711
814
|
try {
|
|
712
815
|
return schema.parse(input);
|
|
@@ -855,6 +958,33 @@ var GetCommentsInputSchema = z.object({
|
|
|
855
958
|
orderBy: z.enum(["created", "-created", "+created"]).optional().describe('Sort order for comments: "created" (oldest first), "-created" (newest first)'),
|
|
856
959
|
startAt: z.number().min(0).optional().describe("Index of first comment to return (for pagination)")
|
|
857
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
|
+
});
|
|
858
988
|
function ensureAdfDescription(desc, format = "markdown") {
|
|
859
989
|
if (!desc) return desc;
|
|
860
990
|
if (typeof desc === "object") return desc;
|
|
@@ -1334,6 +1464,63 @@ async function createIssueLink(fromIssue, toIssue, linkType) {
|
|
|
1334
1464
|
};
|
|
1335
1465
|
await makeJiraRequest(config);
|
|
1336
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
|
+
}
|
|
1337
1524
|
|
|
1338
1525
|
// src/utils/redaction.ts
|
|
1339
1526
|
function isRedactionEnabled() {
|
|
@@ -1814,6 +2001,67 @@ ${fieldsList}`
|
|
|
1814
2001
|
]
|
|
1815
2002
|
};
|
|
1816
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
|
+
}
|
|
1817
2065
|
|
|
1818
2066
|
// src/utils/error-handler.ts
|
|
1819
2067
|
var log2 = createLogger("error-handler");
|
|
@@ -2740,6 +2988,148 @@ async function handleGetComments(input) {
|
|
|
2740
2988
|
}
|
|
2741
2989
|
}
|
|
2742
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
|
+
|
|
2743
3133
|
// src/index.ts
|
|
2744
3134
|
if (process.env.NODE_ENV !== "production") {
|
|
2745
3135
|
try {
|
|
@@ -2748,7 +3138,7 @@ if (process.env.NODE_ENV !== "production") {
|
|
|
2748
3138
|
} catch {
|
|
2749
3139
|
}
|
|
2750
3140
|
}
|
|
2751
|
-
var
|
|
3141
|
+
var log23 = createLogger("server");
|
|
2752
3142
|
var toolHandlers = /* @__PURE__ */ new Map([
|
|
2753
3143
|
[TOOL_NAMES.GET_VISIBLE_PROJECTS, handleGetVisibleProjects],
|
|
2754
3144
|
[TOOL_NAMES.GET_ISSUE, handleGetIssue],
|
|
@@ -2766,7 +3156,10 @@ var toolHandlers = /* @__PURE__ */ new Map([
|
|
|
2766
3156
|
[TOOL_NAMES.GET_CREATE_META, handleGetCreateMeta],
|
|
2767
3157
|
[TOOL_NAMES.GET_CUSTOM_FIELDS, handleGetCustomFields],
|
|
2768
3158
|
[TOOL_NAMES.CREATE_ISSUE_LINK, handleCreateIssueLink],
|
|
2769
|
-
[TOOL_NAMES.GET_COMMENTS, handleGetComments]
|
|
3159
|
+
[TOOL_NAMES.GET_COMMENTS, handleGetComments],
|
|
3160
|
+
[TOOL_NAMES.ADD_ATTACHMENT, handleAddAttachment],
|
|
3161
|
+
[TOOL_NAMES.GET_ATTACHMENTS, handleGetAttachments],
|
|
3162
|
+
[TOOL_NAMES.DELETE_ATTACHMENT, handleDeleteAttachment]
|
|
2770
3163
|
]);
|
|
2771
3164
|
var allTools = [
|
|
2772
3165
|
getVisibleProjectsTool,
|
|
@@ -2785,7 +3178,10 @@ var allTools = [
|
|
|
2785
3178
|
getCreateMetaTool,
|
|
2786
3179
|
getCustomFieldsTool,
|
|
2787
3180
|
createIssueLinkTool,
|
|
2788
|
-
getCommentsTool
|
|
3181
|
+
getCommentsTool,
|
|
3182
|
+
addAttachmentTool,
|
|
3183
|
+
getAttachmentsTool,
|
|
3184
|
+
deleteAttachmentTool
|
|
2789
3185
|
];
|
|
2790
3186
|
async function main() {
|
|
2791
3187
|
const isDryRun = (process.env.DRY_RUN || "").toLowerCase() === "1" || (process.env.DRY_RUN || "").toLowerCase() === "true";
|
|
@@ -2856,7 +3252,7 @@ async function main() {
|
|
|
2856
3252
|
console.error("\u{1F680} Jira MCP server running on stdio");
|
|
2857
3253
|
}
|
|
2858
3254
|
main().catch((error) => {
|
|
2859
|
-
|
|
3255
|
+
log23.error("Fatal error:", error);
|
|
2860
3256
|
process.exit(1);
|
|
2861
3257
|
});
|
|
2862
3258
|
//# sourceMappingURL=index.js.map
|