@rui.branco/jira-mcp 1.6.8 → 1.6.10
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/index.js +348 -53
- package/package.json +1 -1
- package/setup.js +186 -17
package/index.js
CHANGED
|
@@ -46,15 +46,65 @@ try {
|
|
|
46
46
|
}
|
|
47
47
|
} catch {}
|
|
48
48
|
|
|
49
|
-
// Load Jira config
|
|
49
|
+
// Load Jira config (supports single-instance and multi-instance formats)
|
|
50
50
|
const jiraConfigPath = path.join(
|
|
51
51
|
process.env.HOME,
|
|
52
52
|
".config/jira-mcp/config.json",
|
|
53
53
|
);
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const rawConfig = JSON.parse(fs.readFileSync(jiraConfigPath, "utf8"));
|
|
55
|
+
|
|
56
|
+
// Normalize config to multi-instance format
|
|
57
|
+
let instances;
|
|
58
|
+
if (rawConfig.instances) {
|
|
59
|
+
// New multi-instance format
|
|
60
|
+
instances = rawConfig.instances.map((inst) => ({
|
|
61
|
+
...inst,
|
|
62
|
+
projects: (inst.projects || []).map((p) => p.toUpperCase()),
|
|
63
|
+
auth: Buffer.from(`${inst.email}:${inst.token}`).toString("base64"),
|
|
64
|
+
}));
|
|
65
|
+
} else {
|
|
66
|
+
// Old single-instance format — wrap as array
|
|
67
|
+
instances = [
|
|
68
|
+
{
|
|
69
|
+
name: "default",
|
|
70
|
+
email: rawConfig.email,
|
|
71
|
+
token: rawConfig.token,
|
|
72
|
+
baseUrl: rawConfig.baseUrl,
|
|
73
|
+
projects: [],
|
|
74
|
+
auth: Buffer.from(`${rawConfig.email}:${rawConfig.token}`).toString("base64"),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolve default instance
|
|
80
|
+
const defaultInstance =
|
|
81
|
+
(rawConfig.defaultInstance && instances.find((i) => i.name === rawConfig.defaultInstance)) ||
|
|
82
|
+
instances[0];
|
|
83
|
+
|
|
84
|
+
// Re-read config file (for persisting changes)
|
|
85
|
+
function loadConfigFile() {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(fs.readFileSync(jiraConfigPath, "utf8"));
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Instance resolution helpers
|
|
94
|
+
function getInstanceForProject(projectPrefix) {
|
|
95
|
+
const upper = projectPrefix.toUpperCase();
|
|
96
|
+
return instances.find((i) => i.projects.includes(upper)) || defaultInstance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getInstanceForKey(issueKey) {
|
|
100
|
+
const prefix = issueKey.split("-")[0];
|
|
101
|
+
return getInstanceForProject(prefix);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getInstanceByName(name) {
|
|
105
|
+
if (!name) return defaultInstance;
|
|
106
|
+
return instances.find((i) => i.name === name) || defaultInstance;
|
|
107
|
+
}
|
|
58
108
|
|
|
59
109
|
// Load Figma config (optional)
|
|
60
110
|
let figmaConfig = null;
|
|
@@ -86,16 +136,16 @@ if (!fs.existsSync(attachmentDir)) {
|
|
|
86
136
|
|
|
87
137
|
// ============ JIRA FUNCTIONS ============
|
|
88
138
|
|
|
89
|
-
async function fetchJira(endpoint, options = {}) {
|
|
139
|
+
async function fetchJira(endpoint, options = {}, instance = defaultInstance) {
|
|
90
140
|
const { method = "GET", body } = options;
|
|
91
141
|
const headers = {
|
|
92
|
-
Authorization: `Basic ${auth}`,
|
|
142
|
+
Authorization: `Basic ${instance.auth}`,
|
|
93
143
|
Accept: "application/json",
|
|
94
144
|
};
|
|
95
145
|
if (body) {
|
|
96
146
|
headers["Content-Type"] = "application/json";
|
|
97
147
|
}
|
|
98
|
-
const response = await fetch(`${
|
|
148
|
+
const response = await fetch(`${instance.baseUrl}/rest/api/3${endpoint}`, {
|
|
99
149
|
method,
|
|
100
150
|
headers,
|
|
101
151
|
body: body ? JSON.stringify(body) : undefined,
|
|
@@ -110,7 +160,7 @@ async function fetchJira(endpoint, options = {}) {
|
|
|
110
160
|
return text ? JSON.parse(text) : {};
|
|
111
161
|
}
|
|
112
162
|
|
|
113
|
-
async function downloadAttachment(url, filename, issueKey) {
|
|
163
|
+
async function downloadAttachment(url, filename, issueKey, instance) {
|
|
114
164
|
const issueDir = path.join(attachmentDir, issueKey);
|
|
115
165
|
if (!fs.existsSync(issueDir)) {
|
|
116
166
|
fs.mkdirSync(issueDir, { recursive: true });
|
|
@@ -122,8 +172,9 @@ async function downloadAttachment(url, filename, issueKey) {
|
|
|
122
172
|
return localPath;
|
|
123
173
|
}
|
|
124
174
|
|
|
175
|
+
const inst = instance || getInstanceForKey(issueKey);
|
|
125
176
|
const response = await fetch(url, {
|
|
126
|
-
headers: { Authorization: `Basic ${auth}` },
|
|
177
|
+
headers: { Authorization: `Basic ${inst.auth}` },
|
|
127
178
|
});
|
|
128
179
|
|
|
129
180
|
if (!response.ok) {
|
|
@@ -195,9 +246,9 @@ function extractTextSimple(content) {
|
|
|
195
246
|
// Cache for user lookups to avoid repeated API calls
|
|
196
247
|
const userCache = new Map();
|
|
197
248
|
|
|
198
|
-
async function searchUser(query) {
|
|
199
|
-
// Check cache first
|
|
200
|
-
const cacheKey = query.toLowerCase()
|
|
249
|
+
async function searchUser(query, instance = defaultInstance) {
|
|
250
|
+
// Check cache first (instance-aware)
|
|
251
|
+
const cacheKey = `${instance.name}:${query.toLowerCase()}`;
|
|
201
252
|
if (userCache.has(cacheKey)) {
|
|
202
253
|
return userCache.get(cacheKey);
|
|
203
254
|
}
|
|
@@ -206,6 +257,8 @@ async function searchUser(query) {
|
|
|
206
257
|
// Search for users by display name
|
|
207
258
|
const users = await fetchJira(
|
|
208
259
|
`/user/search?query=${encodeURIComponent(query)}&maxResults=5`,
|
|
260
|
+
{},
|
|
261
|
+
instance,
|
|
209
262
|
);
|
|
210
263
|
if (users && users.length > 0) {
|
|
211
264
|
// Find best match - prefer exact match, then starts with, then contains
|
|
@@ -232,7 +285,7 @@ async function searchUser(query) {
|
|
|
232
285
|
|
|
233
286
|
// Parse text with @mentions and build ADF content
|
|
234
287
|
// Parse inline formatting: **bold**, *italic*, @mentions
|
|
235
|
-
async function parseInlineFormatting(text) {
|
|
288
|
+
async function parseInlineFormatting(text, instance = defaultInstance) {
|
|
236
289
|
const nodes = [];
|
|
237
290
|
// Bold (**) must come before italic (*) in alternation, backticks for inline code
|
|
238
291
|
const regex = /(`(.+?)`|\*\*(.+?)\*\*|\*(.+?)\*|@([A-Z][a-zA-Zà-ÿ]*(?:\s[A-Z][a-zA-Zà-ÿ]*)*))/g;
|
|
@@ -256,7 +309,7 @@ async function parseInlineFormatting(text) {
|
|
|
256
309
|
nodes.push({ type: "text", text: match[4], marks: [{ type: "em" }] });
|
|
257
310
|
} else if (match[5] !== undefined) {
|
|
258
311
|
// @Mention
|
|
259
|
-
const user = await searchUser(match[5].trim());
|
|
312
|
+
const user = await searchUser(match[5].trim(), instance);
|
|
260
313
|
if (user) {
|
|
261
314
|
nodes.push({
|
|
262
315
|
type: "mention",
|
|
@@ -278,7 +331,7 @@ async function parseInlineFormatting(text) {
|
|
|
278
331
|
}
|
|
279
332
|
|
|
280
333
|
// Parse text with markdown formatting and @mentions, build ADF content
|
|
281
|
-
async function buildCommentADF(text) {
|
|
334
|
+
async function buildCommentADF(text, instance = defaultInstance) {
|
|
282
335
|
// Sanitize: replace em dashes and en dashes with hyphen
|
|
283
336
|
text = text.replace(/[—–]/g, "-");
|
|
284
337
|
// Split into blocks by double newlines (paragraphs)
|
|
@@ -297,7 +350,7 @@ async function buildCommentADF(text) {
|
|
|
297
350
|
const listItems = [];
|
|
298
351
|
for (const line of lines) {
|
|
299
352
|
const itemText = line.trimStart().substring(2);
|
|
300
|
-
const inlineContent = await parseInlineFormatting(itemText);
|
|
353
|
+
const inlineContent = await parseInlineFormatting(itemText, instance);
|
|
301
354
|
listItems.push({
|
|
302
355
|
type: "listItem",
|
|
303
356
|
content: [{ type: "paragraph", content: inlineContent }],
|
|
@@ -309,7 +362,7 @@ async function buildCommentADF(text) {
|
|
|
309
362
|
const paragraphContent = [];
|
|
310
363
|
for (let i = 0; i < lines.length; i++) {
|
|
311
364
|
if (i > 0) paragraphContent.push({ type: "hardBreak" });
|
|
312
|
-
const inlineNodes = await parseInlineFormatting(lines[i]);
|
|
365
|
+
const inlineNodes = await parseInlineFormatting(lines[i], instance);
|
|
313
366
|
paragraphContent.push(...inlineNodes);
|
|
314
367
|
}
|
|
315
368
|
content.push({ type: "paragraph", content: paragraphContent });
|
|
@@ -592,8 +645,9 @@ async function fetchFigmaDesign(url) {
|
|
|
592
645
|
|
|
593
646
|
// ============ MAIN TICKET FUNCTION ============
|
|
594
647
|
|
|
595
|
-
async function getTicket(issueKey, downloadImages = true, fetchFigma = true) {
|
|
596
|
-
|
|
648
|
+
async function getTicket(issueKey, downloadImages = true, fetchFigma = true, instance = null) {
|
|
649
|
+
instance = instance || getInstanceForKey(issueKey);
|
|
650
|
+
const issue = await fetchJira(`/issue/${issueKey}?expand=renderedFields`, {}, instance);
|
|
597
651
|
const fields = issue.fields;
|
|
598
652
|
const storyPoints = fields.customfield_10016 ?? fields.story_points ?? null;
|
|
599
653
|
|
|
@@ -651,7 +705,7 @@ async function getTicket(issueKey, downloadImages = true, fetchFigma = true) {
|
|
|
651
705
|
output += `\n## Parent Ticket: ${fields.parent.key}\n\n`;
|
|
652
706
|
try {
|
|
653
707
|
const parentIssue = await fetchJira(
|
|
654
|
-
`/issue/${fields.parent.key}?expand=renderedFields`,
|
|
708
|
+
`/issue/${fields.parent.key}?expand=renderedFields`, {}, instance,
|
|
655
709
|
);
|
|
656
710
|
const pf = parentIssue.fields;
|
|
657
711
|
|
|
@@ -716,6 +770,7 @@ async function getTicket(issueKey, downloadImages = true, fetchFigma = true) {
|
|
|
716
770
|
att.content,
|
|
717
771
|
att.filename,
|
|
718
772
|
issueKey,
|
|
773
|
+
instance,
|
|
719
774
|
);
|
|
720
775
|
output += ` Local: ${localPath}\n`;
|
|
721
776
|
downloadedImages.push(localPath);
|
|
@@ -738,7 +793,7 @@ async function getTicket(issueKey, downloadImages = true, fetchFigma = true) {
|
|
|
738
793
|
output += `Type: ${subtask.fields?.issuetype?.name || "Subtask"}\n`;
|
|
739
794
|
|
|
740
795
|
try {
|
|
741
|
-
const subtaskDetails = await fetchJira(`/issue/${subtask.key}
|
|
796
|
+
const subtaskDetails = await fetchJira(`/issue/${subtask.key}`, {}, instance);
|
|
742
797
|
const sf = subtaskDetails.fields;
|
|
743
798
|
|
|
744
799
|
if (sf.assignee) {
|
|
@@ -792,7 +847,7 @@ async function getTicket(issueKey, downloadImages = true, fetchFigma = true) {
|
|
|
792
847
|
|
|
793
848
|
try {
|
|
794
849
|
const linkedIssue = await fetchJira(
|
|
795
|
-
`/issue/${linked.key}?expand=renderedFields`,
|
|
850
|
+
`/issue/${linked.key}?expand=renderedFields`, {}, instance,
|
|
796
851
|
);
|
|
797
852
|
const lf = linkedIssue.fields;
|
|
798
853
|
|
|
@@ -864,7 +919,7 @@ async function getTicket(issueKey, downloadImages = true, fetchFigma = true) {
|
|
|
864
919
|
|
|
865
920
|
try {
|
|
866
921
|
const refIssue = await fetchJira(
|
|
867
|
-
`/issue/${refKey}?expand=renderedFields`,
|
|
922
|
+
`/issue/${refKey}?expand=renderedFields`, {}, instance,
|
|
868
923
|
);
|
|
869
924
|
const rf = refIssue.fields;
|
|
870
925
|
|
|
@@ -950,7 +1005,7 @@ async function getTicket(issueKey, downloadImages = true, fetchFigma = true) {
|
|
|
950
1005
|
return { text: output, jiraImages: downloadedImages, figmaDesigns };
|
|
951
1006
|
}
|
|
952
1007
|
|
|
953
|
-
async function searchTickets(jql, maxResults = 10, fields = null) {
|
|
1008
|
+
async function searchTickets(jql, maxResults = 10, fields = null, instance = defaultInstance) {
|
|
954
1009
|
const defaultFields = [
|
|
955
1010
|
"summary", "status", "assignee", "reporter", "issuetype", "priority",
|
|
956
1011
|
"created", "resolutiondate", "updated", "statuscategorychangedate",
|
|
@@ -961,6 +1016,8 @@ async function searchTickets(jql, maxResults = 10, fields = null) {
|
|
|
961
1016
|
const requestFields = fields || defaultFields;
|
|
962
1017
|
const data = await fetchJira(
|
|
963
1018
|
`/search/jql?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=${requestFields.join(",")}`,
|
|
1019
|
+
{},
|
|
1020
|
+
instance,
|
|
964
1021
|
);
|
|
965
1022
|
|
|
966
1023
|
const issues = data.issues || [];
|
|
@@ -1122,17 +1179,20 @@ function formatChangelog(issueKey, statusHistory, created) {
|
|
|
1122
1179
|
return output;
|
|
1123
1180
|
}
|
|
1124
1181
|
|
|
1125
|
-
async function getChangelog(issueKey) {
|
|
1126
|
-
|
|
1182
|
+
async function getChangelog(issueKey, instance = null) {
|
|
1183
|
+
instance = instance || getInstanceForKey(issueKey);
|
|
1184
|
+
const issue = await fetchJira(`/issue/${issueKey}?expand=changelog&fields=created`, {}, instance);
|
|
1127
1185
|
const statusHistory = parseStatusHistory(issue.changelog);
|
|
1128
1186
|
const created = issue.fields?.created;
|
|
1129
1187
|
return { statusHistory, created, formatted: formatChangelog(issueKey, statusHistory, created) };
|
|
1130
1188
|
}
|
|
1131
1189
|
|
|
1132
|
-
async function getChangelogsBulk(jql, maxResults = 50) {
|
|
1190
|
+
async function getChangelogsBulk(jql, maxResults = 50, instance = defaultInstance) {
|
|
1133
1191
|
// First get the issue keys matching the JQL
|
|
1134
1192
|
const data = await fetchJira(
|
|
1135
1193
|
`/search/jql?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=key,created`,
|
|
1194
|
+
{},
|
|
1195
|
+
instance,
|
|
1136
1196
|
);
|
|
1137
1197
|
const issues = data.issues || [];
|
|
1138
1198
|
|
|
@@ -1141,7 +1201,7 @@ async function getChangelogsBulk(jql, maxResults = 50) {
|
|
|
1141
1201
|
// Fetch changelog for each issue (JIRA search API doesn't support expand=changelog on /search/jql)
|
|
1142
1202
|
for (const issue of issues) {
|
|
1143
1203
|
try {
|
|
1144
|
-
const result = await getChangelog(issue.key);
|
|
1204
|
+
const result = await getChangelog(issue.key, instance);
|
|
1145
1205
|
output += result.formatted + "\n";
|
|
1146
1206
|
} catch (e) {
|
|
1147
1207
|
output += `## ${issue.key}\n\n_Error fetching changelog: ${e.message}_\n\n`;
|
|
@@ -1164,10 +1224,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1164
1224
|
{
|
|
1165
1225
|
name: "jira_get_myself",
|
|
1166
1226
|
description:
|
|
1167
|
-
"Get the current authenticated user's info including accountId. Use this to get your account ID for assigning tickets.",
|
|
1227
|
+
"Get the current authenticated user's info including accountId. Use this to get your account ID for assigning tickets. When multiple Jira instances are configured, use the 'instance' parameter to specify which one.",
|
|
1168
1228
|
inputSchema: {
|
|
1169
1229
|
type: "object",
|
|
1170
|
-
properties: {
|
|
1230
|
+
properties: {
|
|
1231
|
+
instance: {
|
|
1232
|
+
type: "string",
|
|
1233
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1171
1236
|
required: [],
|
|
1172
1237
|
},
|
|
1173
1238
|
},
|
|
@@ -1191,6 +1256,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1191
1256
|
description:
|
|
1192
1257
|
"Fetch linked Figma designs and export images (default: true)",
|
|
1193
1258
|
},
|
|
1259
|
+
instance: {
|
|
1260
|
+
type: "string",
|
|
1261
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1262
|
+
},
|
|
1194
1263
|
},
|
|
1195
1264
|
required: ["issueKey"],
|
|
1196
1265
|
},
|
|
@@ -1198,7 +1267,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1198
1267
|
{
|
|
1199
1268
|
name: "jira_search",
|
|
1200
1269
|
description:
|
|
1201
|
-
"Search Jira tickets using JQL. Returns detailed fields including dates, story points, labels, components, fix versions, time tracking, and parent. Examples: 'project = MODS AND status = Open'",
|
|
1270
|
+
"Search Jira tickets using JQL. Returns detailed fields including dates, story points, labels, components, fix versions, time tracking, and parent. Examples: 'project = MODS AND status = Open'. When multiple Jira instances are configured, use the 'instance' parameter to specify which one.",
|
|
1202
1271
|
inputSchema: {
|
|
1203
1272
|
type: "object",
|
|
1204
1273
|
properties: {
|
|
@@ -1213,6 +1282,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1213
1282
|
description:
|
|
1214
1283
|
"Custom list of fields to return. Default includes: summary, status, assignee, reporter, issuetype, priority, created, resolutiondate, updated, resolution, timetracking, parent, labels, components, fixVersions, customfield_10016 (story points).",
|
|
1215
1284
|
},
|
|
1285
|
+
instance: {
|
|
1286
|
+
type: "string",
|
|
1287
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
1288
|
+
},
|
|
1216
1289
|
},
|
|
1217
1290
|
required: ["jql"],
|
|
1218
1291
|
},
|
|
@@ -1229,6 +1302,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1229
1302
|
description: "The Jira issue key (e.g., MODS-123)",
|
|
1230
1303
|
},
|
|
1231
1304
|
comment: { type: "string", description: "The comment text to add" },
|
|
1305
|
+
instance: {
|
|
1306
|
+
type: "string",
|
|
1307
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1308
|
+
},
|
|
1232
1309
|
},
|
|
1233
1310
|
required: ["issueKey", "comment"],
|
|
1234
1311
|
},
|
|
@@ -1250,6 +1327,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1250
1327
|
"The ID of the comment to reply to. Use jira_get_ticket to see comments and their IDs.",
|
|
1251
1328
|
},
|
|
1252
1329
|
reply: { type: "string", description: "The reply text" },
|
|
1330
|
+
instance: {
|
|
1331
|
+
type: "string",
|
|
1332
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1333
|
+
},
|
|
1253
1334
|
},
|
|
1254
1335
|
required: ["issueKey", "commentId", "reply"],
|
|
1255
1336
|
},
|
|
@@ -1271,6 +1352,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1271
1352
|
"The ID of the comment to edit. Use jira_get_ticket to see comments and their IDs.",
|
|
1272
1353
|
},
|
|
1273
1354
|
comment: { type: "string", description: "The new comment text" },
|
|
1355
|
+
instance: {
|
|
1356
|
+
type: "string",
|
|
1357
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1358
|
+
},
|
|
1274
1359
|
},
|
|
1275
1360
|
required: ["issueKey", "commentId", "comment"],
|
|
1276
1361
|
},
|
|
@@ -1291,6 +1376,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1291
1376
|
description:
|
|
1292
1377
|
"The ID of the comment to delete. Use jira_get_ticket to see comments and their IDs.",
|
|
1293
1378
|
},
|
|
1379
|
+
instance: {
|
|
1380
|
+
type: "string",
|
|
1381
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1382
|
+
},
|
|
1294
1383
|
},
|
|
1295
1384
|
required: ["issueKey", "commentId"],
|
|
1296
1385
|
},
|
|
@@ -1316,6 +1405,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1316
1405
|
description:
|
|
1317
1406
|
"Target status name (e.g., 'Review', 'Done'). Will auto-transition through intermediate states if needed.",
|
|
1318
1407
|
},
|
|
1408
|
+
instance: {
|
|
1409
|
+
type: "string",
|
|
1410
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1411
|
+
},
|
|
1319
1412
|
},
|
|
1320
1413
|
required: ["issueKey"],
|
|
1321
1414
|
},
|
|
@@ -1369,6 +1462,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1369
1462
|
items: { type: "string" },
|
|
1370
1463
|
description: "Labels to set on the ticket",
|
|
1371
1464
|
},
|
|
1465
|
+
instance: {
|
|
1466
|
+
type: "string",
|
|
1467
|
+
description: "Instance name override. Auto-detected from issue key prefix if omitted.",
|
|
1468
|
+
},
|
|
1372
1469
|
},
|
|
1373
1470
|
required: ["issueKey"],
|
|
1374
1471
|
},
|
|
@@ -1376,7 +1473,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1376
1473
|
{
|
|
1377
1474
|
name: "jira_search_users",
|
|
1378
1475
|
description:
|
|
1379
|
-
"Search for Jira users by name or email. Returns account IDs and display names. Use this to find users for mentions or assignments.",
|
|
1476
|
+
"Search for Jira users by name or email. Returns account IDs and display names. Use this to find users for mentions or assignments. When multiple Jira instances are configured, use the 'instance' parameter to specify which one.",
|
|
1380
1477
|
inputSchema: {
|
|
1381
1478
|
type: "object",
|
|
1382
1479
|
properties: {
|
|
@@ -1389,6 +1486,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1389
1486
|
type: "number",
|
|
1390
1487
|
description: "Max results (default 5)",
|
|
1391
1488
|
},
|
|
1489
|
+
instance: {
|
|
1490
|
+
type: "string",
|
|
1491
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
1492
|
+
},
|
|
1392
1493
|
},
|
|
1393
1494
|
required: ["query"],
|
|
1394
1495
|
},
|
|
@@ -1396,7 +1497,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1396
1497
|
{
|
|
1397
1498
|
name: "jira_get_changelog",
|
|
1398
1499
|
description:
|
|
1399
|
-
"Get status change history for a Jira ticket or multiple tickets via JQL. Returns all status transitions with timestamps and authors, plus computed metrics (cycle time, lead time, time in each status). Use issueKey for a single ticket, or jql for bulk retrieval.",
|
|
1500
|
+
"Get status change history for a Jira ticket or multiple tickets via JQL. Returns all status transitions with timestamps and authors, plus computed metrics (cycle time, lead time, time in each status). Use issueKey for a single ticket, or jql for bulk retrieval. Instance is auto-detected from issueKey, or use the 'instance' parameter with jql.",
|
|
1400
1501
|
inputSchema: {
|
|
1401
1502
|
type: "object",
|
|
1402
1503
|
properties: {
|
|
@@ -1415,10 +1516,75 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1415
1516
|
description:
|
|
1416
1517
|
"Max results when using jql (default 50). Each ticket requires a separate API call, so keep this reasonable.",
|
|
1417
1518
|
},
|
|
1519
|
+
instance: {
|
|
1520
|
+
type: "string",
|
|
1521
|
+
description: "Instance name (for multi-instance setups with jql). Auto-detected from issueKey if provided.",
|
|
1522
|
+
},
|
|
1418
1523
|
},
|
|
1419
1524
|
required: [],
|
|
1420
1525
|
},
|
|
1421
1526
|
},
|
|
1527
|
+
{
|
|
1528
|
+
name: "jira_add_instance",
|
|
1529
|
+
description:
|
|
1530
|
+
"Add or update a Jira instance configuration. Saves to config and makes it available immediately without restart. Use this to connect to a new Jira instance during a session.",
|
|
1531
|
+
inputSchema: {
|
|
1532
|
+
type: "object",
|
|
1533
|
+
properties: {
|
|
1534
|
+
name: {
|
|
1535
|
+
type: "string",
|
|
1536
|
+
description: "Unique name for this instance (e.g., 'work', 'personal')",
|
|
1537
|
+
},
|
|
1538
|
+
email: {
|
|
1539
|
+
type: "string",
|
|
1540
|
+
description: "Jira account email",
|
|
1541
|
+
},
|
|
1542
|
+
token: {
|
|
1543
|
+
type: "string",
|
|
1544
|
+
description: "Jira API token (from https://id.atlassian.com/manage-profile/security/api-tokens)",
|
|
1545
|
+
},
|
|
1546
|
+
baseUrl: {
|
|
1547
|
+
type: "string",
|
|
1548
|
+
description: "Jira base URL (e.g., https://company.atlassian.net)",
|
|
1549
|
+
},
|
|
1550
|
+
projects: {
|
|
1551
|
+
type: "array",
|
|
1552
|
+
items: { type: "string" },
|
|
1553
|
+
description: "Project key prefixes to auto-route to this instance (e.g., ['PROJ', 'ENG'])",
|
|
1554
|
+
},
|
|
1555
|
+
setDefault: {
|
|
1556
|
+
type: "boolean",
|
|
1557
|
+
description: "Set this instance as the default (default: false)",
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1560
|
+
required: ["name", "email", "token", "baseUrl"],
|
|
1561
|
+
},
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
name: "jira_remove_instance",
|
|
1565
|
+
description:
|
|
1566
|
+
"Remove a Jira instance configuration by name. Cannot remove the last remaining instance.",
|
|
1567
|
+
inputSchema: {
|
|
1568
|
+
type: "object",
|
|
1569
|
+
properties: {
|
|
1570
|
+
name: {
|
|
1571
|
+
type: "string",
|
|
1572
|
+
description: "Name of the instance to remove",
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
required: ["name"],
|
|
1576
|
+
},
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
name: "jira_list_instances",
|
|
1580
|
+
description:
|
|
1581
|
+
"List all configured Jira instances with their names, URLs, project prefixes, and which is the default.",
|
|
1582
|
+
inputSchema: {
|
|
1583
|
+
type: "object",
|
|
1584
|
+
properties: {},
|
|
1585
|
+
required: [],
|
|
1586
|
+
},
|
|
1587
|
+
},
|
|
1422
1588
|
],
|
|
1423
1589
|
};
|
|
1424
1590
|
});
|
|
@@ -1428,7 +1594,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1428
1594
|
|
|
1429
1595
|
try {
|
|
1430
1596
|
if (name === "jira_get_myself") {
|
|
1431
|
-
const
|
|
1597
|
+
const inst = getInstanceByName(args.instance);
|
|
1598
|
+
const result = await fetchJira("/myself", {}, inst);
|
|
1432
1599
|
return {
|
|
1433
1600
|
content: [
|
|
1434
1601
|
{
|
|
@@ -1438,9 +1605,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1438
1605
|
],
|
|
1439
1606
|
};
|
|
1440
1607
|
} else if (name === "jira_search_users") {
|
|
1608
|
+
const inst = getInstanceByName(args.instance);
|
|
1441
1609
|
const maxResults = args.maxResults || 5;
|
|
1442
1610
|
const users = await fetchJira(
|
|
1443
1611
|
`/user/search?query=${encodeURIComponent(args.query)}&maxResults=${maxResults}`,
|
|
1612
|
+
{},
|
|
1613
|
+
inst,
|
|
1444
1614
|
);
|
|
1445
1615
|
if (!users || users.length === 0) {
|
|
1446
1616
|
return {
|
|
@@ -1467,7 +1637,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1467
1637
|
} else if (name === "jira_get_ticket") {
|
|
1468
1638
|
const downloadImages = args.downloadImages !== false;
|
|
1469
1639
|
const fetchFigma = args.fetchFigma !== false;
|
|
1470
|
-
const
|
|
1640
|
+
const inst = args.instance ? getInstanceByName(args.instance) : null;
|
|
1641
|
+
const result = await getTicket(args.issueKey, downloadImages, fetchFigma, inst);
|
|
1471
1642
|
|
|
1472
1643
|
const content = [{ type: "text", text: result.text }];
|
|
1473
1644
|
|
|
@@ -1511,11 +1682,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1511
1682
|
|
|
1512
1683
|
return { content };
|
|
1513
1684
|
} else if (name === "jira_search") {
|
|
1514
|
-
const
|
|
1685
|
+
const inst = getInstanceByName(args.instance);
|
|
1686
|
+
const result = await searchTickets(args.jql, args.maxResults || 10, args.fields || null, inst);
|
|
1515
1687
|
return { content: [{ type: "text", text: result }] };
|
|
1516
1688
|
} else if (name === "jira_add_comment") {
|
|
1689
|
+
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
1517
1690
|
// Build ADF content with mention support
|
|
1518
|
-
const adfContent = await buildCommentADF(args.comment);
|
|
1691
|
+
const adfContent = await buildCommentADF(args.comment, inst);
|
|
1519
1692
|
const body = {
|
|
1520
1693
|
body: {
|
|
1521
1694
|
version: 1,
|
|
@@ -1526,7 +1699,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1526
1699
|
const result = await fetchJira(`/issue/${args.issueKey}/comment`, {
|
|
1527
1700
|
method: "POST",
|
|
1528
1701
|
body,
|
|
1529
|
-
});
|
|
1702
|
+
}, inst);
|
|
1530
1703
|
const author = result.author?.displayName || "Unknown";
|
|
1531
1704
|
const created = new Date(result.created).toLocaleString();
|
|
1532
1705
|
return {
|
|
@@ -1538,9 +1711,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1538
1711
|
],
|
|
1539
1712
|
};
|
|
1540
1713
|
} else if (name === "jira_reply_comment") {
|
|
1714
|
+
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
1541
1715
|
// Fetch the original comment
|
|
1542
1716
|
const original = await fetchJira(
|
|
1543
1717
|
`/issue/${args.issueKey}/comment/${args.commentId}`,
|
|
1718
|
+
{},
|
|
1719
|
+
inst,
|
|
1544
1720
|
);
|
|
1545
1721
|
const originalAuthor = original.author?.displayName || "Unknown";
|
|
1546
1722
|
const originalAccountId = original.author?.accountId;
|
|
@@ -1591,7 +1767,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1591
1767
|
const result = await fetchJira(`/issue/${args.issueKey}/comment`, {
|
|
1592
1768
|
method: "POST",
|
|
1593
1769
|
body,
|
|
1594
|
-
});
|
|
1770
|
+
}, inst);
|
|
1595
1771
|
const author = result.author?.displayName || "Unknown";
|
|
1596
1772
|
const created = new Date(result.created).toLocaleString();
|
|
1597
1773
|
return {
|
|
@@ -1603,8 +1779,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1603
1779
|
],
|
|
1604
1780
|
};
|
|
1605
1781
|
} else if (name === "jira_edit_comment") {
|
|
1782
|
+
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
1606
1783
|
// Build ADF content with mention support
|
|
1607
|
-
const adfContent = await buildCommentADF(args.comment);
|
|
1784
|
+
const adfContent = await buildCommentADF(args.comment, inst);
|
|
1608
1785
|
const body = {
|
|
1609
1786
|
body: {
|
|
1610
1787
|
version: 1,
|
|
@@ -1615,6 +1792,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1615
1792
|
const result = await fetchJira(
|
|
1616
1793
|
`/issue/${args.issueKey}/comment/${args.commentId}`,
|
|
1617
1794
|
{ method: "PUT", body },
|
|
1795
|
+
inst,
|
|
1618
1796
|
);
|
|
1619
1797
|
return {
|
|
1620
1798
|
content: [
|
|
@@ -1625,9 +1803,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1625
1803
|
],
|
|
1626
1804
|
};
|
|
1627
1805
|
} else if (name === "jira_delete_comment") {
|
|
1806
|
+
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
1628
1807
|
await fetchJira(`/issue/${args.issueKey}/comment/${args.commentId}`, {
|
|
1629
1808
|
method: "DELETE",
|
|
1630
|
-
});
|
|
1809
|
+
}, inst);
|
|
1631
1810
|
return {
|
|
1632
1811
|
content: [
|
|
1633
1812
|
{
|
|
@@ -1637,9 +1816,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1637
1816
|
],
|
|
1638
1817
|
};
|
|
1639
1818
|
} else if (name === "jira_transition") {
|
|
1819
|
+
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
1640
1820
|
if (!args.transitionId && !args.targetStatus) {
|
|
1641
1821
|
// List available transitions
|
|
1642
|
-
const result = await fetchJira(`/issue/${args.issueKey}/transitions
|
|
1822
|
+
const result = await fetchJira(`/issue/${args.issueKey}/transitions`, {}, inst);
|
|
1643
1823
|
let output = `# Available transitions for ${args.issueKey}\n\n`;
|
|
1644
1824
|
for (const t of result.transitions || []) {
|
|
1645
1825
|
output += `- **${t.name}** (id: ${t.id}) → status: ${t.to?.name || "Unknown"}\n`;
|
|
@@ -1657,7 +1837,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1657
1837
|
|
|
1658
1838
|
// Try to reach target status, with up to 3 intermediate transitions
|
|
1659
1839
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1660
|
-
const result = await fetchJira(`/issue/${args.issueKey}/transitions
|
|
1840
|
+
const result = await fetchJira(`/issue/${args.issueKey}/transitions`, {}, inst);
|
|
1661
1841
|
const available = result.transitions || [];
|
|
1662
1842
|
|
|
1663
1843
|
// Check if target status is directly available
|
|
@@ -1671,7 +1851,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1671
1851
|
await fetchJira(`/issue/${args.issueKey}/transitions`, {
|
|
1672
1852
|
method: "POST",
|
|
1673
1853
|
body: { transition: { id: directMatch.id } },
|
|
1674
|
-
});
|
|
1854
|
+
}, inst);
|
|
1675
1855
|
transitions.push(directMatch.to?.name || directMatch.name);
|
|
1676
1856
|
return {
|
|
1677
1857
|
content: [
|
|
@@ -1694,7 +1874,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1694
1874
|
await fetchJira(`/issue/${args.issueKey}/transitions`, {
|
|
1695
1875
|
method: "POST",
|
|
1696
1876
|
body: { transition: { id: inProgress.id } },
|
|
1697
|
-
});
|
|
1877
|
+
}, inst);
|
|
1698
1878
|
transitions.push(inProgress.to?.name || "In Progress");
|
|
1699
1879
|
continue; // Try again to find target
|
|
1700
1880
|
}
|
|
@@ -1704,7 +1884,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1704
1884
|
}
|
|
1705
1885
|
|
|
1706
1886
|
// Could not reach target status
|
|
1707
|
-
const result = await fetchJira(`/issue/${args.issueKey}/transitions
|
|
1887
|
+
const result = await fetchJira(`/issue/${args.issueKey}/transitions`, {}, inst);
|
|
1708
1888
|
const availableNames = (result.transitions || [])
|
|
1709
1889
|
.map((t) => t.to?.name || t.name)
|
|
1710
1890
|
.join(", ");
|
|
@@ -1722,7 +1902,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1722
1902
|
await fetchJira(`/issue/${args.issueKey}/transitions`, {
|
|
1723
1903
|
method: "POST",
|
|
1724
1904
|
body: { transition: { id: args.transitionId } },
|
|
1725
|
-
});
|
|
1905
|
+
}, inst);
|
|
1726
1906
|
return {
|
|
1727
1907
|
content: [
|
|
1728
1908
|
{
|
|
@@ -1732,6 +1912,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1732
1912
|
],
|
|
1733
1913
|
};
|
|
1734
1914
|
} else if (name === "jira_update_ticket") {
|
|
1915
|
+
const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
|
|
1735
1916
|
const fields = {};
|
|
1736
1917
|
if (args.summary) {
|
|
1737
1918
|
if (args.replaceSummary) {
|
|
@@ -1739,7 +1920,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1739
1920
|
} else {
|
|
1740
1921
|
// Append to existing title (default)
|
|
1741
1922
|
const issue = await fetchJira(
|
|
1742
|
-
`/issue/${args.issueKey}?fields=summary`,
|
|
1923
|
+
`/issue/${args.issueKey}?fields=summary`, {}, inst,
|
|
1743
1924
|
);
|
|
1744
1925
|
const existing = issue.fields?.summary || "";
|
|
1745
1926
|
fields.summary = existing + " " + args.summary;
|
|
@@ -1760,7 +1941,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1760
1941
|
} else {
|
|
1761
1942
|
// Append to existing (default)
|
|
1762
1943
|
const issue = await fetchJira(
|
|
1763
|
-
`/issue/${args.issueKey}?fields=description`,
|
|
1944
|
+
`/issue/${args.issueKey}?fields=description`, {}, inst,
|
|
1764
1945
|
);
|
|
1765
1946
|
const existing = issue.fields?.description;
|
|
1766
1947
|
if (existing && existing.content) {
|
|
@@ -1777,7 +1958,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1777
1958
|
}
|
|
1778
1959
|
if (args.removeFromDescription) {
|
|
1779
1960
|
const issue = await fetchJira(
|
|
1780
|
-
`/issue/${args.issueKey}?fields=description`,
|
|
1961
|
+
`/issue/${args.issueKey}?fields=description`, {}, inst,
|
|
1781
1962
|
);
|
|
1782
1963
|
const existing = issue.fields?.description;
|
|
1783
1964
|
if (existing && existing.content) {
|
|
@@ -1830,7 +2011,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1830
2011
|
await fetchJira(`/issue/${args.issueKey}`, {
|
|
1831
2012
|
method: "PUT",
|
|
1832
2013
|
body: { fields },
|
|
1833
|
-
});
|
|
2014
|
+
}, inst);
|
|
1834
2015
|
const updated = Object.keys(fields).join(", ");
|
|
1835
2016
|
return {
|
|
1836
2017
|
content: [
|
|
@@ -1848,9 +2029,123 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1848
2029
|
const result = await getChangelog(args.issueKey);
|
|
1849
2030
|
return { content: [{ type: "text", text: result.formatted }] };
|
|
1850
2031
|
} else {
|
|
1851
|
-
const
|
|
2032
|
+
const inst = getInstanceByName(args.instance);
|
|
2033
|
+
const result = await getChangelogsBulk(args.jql, args.maxResults || 50, inst);
|
|
1852
2034
|
return { content: [{ type: "text", text: result }] };
|
|
1853
2035
|
}
|
|
2036
|
+
} else if (name === "jira_add_instance") {
|
|
2037
|
+
const instName = args.name.trim();
|
|
2038
|
+
const projects = (args.projects || []).map((p) => p.toUpperCase());
|
|
2039
|
+
const authStr = Buffer.from(`${args.email}:${args.token}`).toString("base64");
|
|
2040
|
+
const newInstance = {
|
|
2041
|
+
name: instName,
|
|
2042
|
+
email: args.email,
|
|
2043
|
+
token: args.token,
|
|
2044
|
+
baseUrl: args.baseUrl.replace(/\/$/, ""),
|
|
2045
|
+
projects,
|
|
2046
|
+
auth: authStr,
|
|
2047
|
+
};
|
|
2048
|
+
|
|
2049
|
+
// Update in-memory instances
|
|
2050
|
+
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
2051
|
+
if (existingIdx >= 0) {
|
|
2052
|
+
instances[existingIdx] = newInstance;
|
|
2053
|
+
} else {
|
|
2054
|
+
instances.push(newInstance);
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// Update default if requested or if it's the first instance
|
|
2058
|
+
if (args.setDefault || instances.length === 1) {
|
|
2059
|
+
// Can't reassign const, but defaultInstance is used via getInstanceByName/getInstanceForKey
|
|
2060
|
+
// which search the instances array, so this is handled by rawConfig.defaultInstance below
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// Persist to config file
|
|
2064
|
+
const savedConfig = loadConfigFile();
|
|
2065
|
+
if (!savedConfig.instances) {
|
|
2066
|
+
// Migrate old format
|
|
2067
|
+
if (savedConfig.email) {
|
|
2068
|
+
savedConfig.instances = [{
|
|
2069
|
+
name: "default",
|
|
2070
|
+
email: savedConfig.email,
|
|
2071
|
+
token: savedConfig.token,
|
|
2072
|
+
baseUrl: savedConfig.baseUrl,
|
|
2073
|
+
projects: [],
|
|
2074
|
+
}];
|
|
2075
|
+
savedConfig.defaultInstance = "default";
|
|
2076
|
+
delete savedConfig.email;
|
|
2077
|
+
delete savedConfig.token;
|
|
2078
|
+
delete savedConfig.baseUrl;
|
|
2079
|
+
} else {
|
|
2080
|
+
savedConfig.instances = [];
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
// Save without the computed auth field
|
|
2085
|
+
const toSave = { name: instName, email: args.email, token: args.token, baseUrl: newInstance.baseUrl, projects };
|
|
2086
|
+
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
2087
|
+
if (savedIdx >= 0) {
|
|
2088
|
+
savedConfig.instances[savedIdx] = toSave;
|
|
2089
|
+
} else {
|
|
2090
|
+
savedConfig.instances.push(toSave);
|
|
2091
|
+
}
|
|
2092
|
+
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
2093
|
+
savedConfig.defaultInstance = instName;
|
|
2094
|
+
}
|
|
2095
|
+
fs.writeFileSync(jiraConfigPath, JSON.stringify(savedConfig, null, 2));
|
|
2096
|
+
|
|
2097
|
+
const action = existingIdx >= 0 ? "Updated" : "Added";
|
|
2098
|
+
let text = `${action} instance "${instName}" (${newInstance.baseUrl}).`;
|
|
2099
|
+
if (projects.length > 0) text += ` Projects: ${projects.join(", ")}.`;
|
|
2100
|
+
if (args.setDefault) text += " Set as default.";
|
|
2101
|
+
|
|
2102
|
+
return { content: [{ type: "text", text }] };
|
|
2103
|
+
|
|
2104
|
+
} else if (name === "jira_remove_instance") {
|
|
2105
|
+
const instName = args.name.trim();
|
|
2106
|
+
|
|
2107
|
+
if (instances.length <= 1) {
|
|
2108
|
+
return {
|
|
2109
|
+
content: [{ type: "text", text: "Cannot remove the last remaining instance." }],
|
|
2110
|
+
isError: true,
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const idx = instances.findIndex((i) => i.name === instName);
|
|
2115
|
+
if (idx < 0) {
|
|
2116
|
+
return {
|
|
2117
|
+
content: [{ type: "text", text: `Instance "${instName}" not found.` }],
|
|
2118
|
+
isError: true,
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
instances.splice(idx, 1);
|
|
2123
|
+
|
|
2124
|
+
// Persist to config file
|
|
2125
|
+
const savedConfig = loadConfigFile();
|
|
2126
|
+
if (savedConfig.instances) {
|
|
2127
|
+
savedConfig.instances = savedConfig.instances.filter((i) => i.name !== instName);
|
|
2128
|
+
if (savedConfig.defaultInstance === instName) {
|
|
2129
|
+
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
2130
|
+
}
|
|
2131
|
+
fs.writeFileSync(jiraConfigPath, JSON.stringify(savedConfig, null, 2));
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
return { content: [{ type: "text", text: `Removed instance "${instName}".` }] };
|
|
2135
|
+
|
|
2136
|
+
} else if (name === "jira_list_instances") {
|
|
2137
|
+
if (instances.length === 0) {
|
|
2138
|
+
return { content: [{ type: "text", text: "No instances configured." }] };
|
|
2139
|
+
}
|
|
2140
|
+
const currentDefault = rawConfig.defaultInstance || instances[0].name;
|
|
2141
|
+
let text = `# Configured Jira Instances (${instances.length})\n\n`;
|
|
2142
|
+
for (const inst of instances) {
|
|
2143
|
+
const isDefault = inst.name === currentDefault ? " **(default)**" : "";
|
|
2144
|
+
const projs = inst.projects?.length > 0 ? `\n Projects: ${inst.projects.join(", ")}` : "";
|
|
2145
|
+
text += `- **${inst.name}**${isDefault}: ${inst.baseUrl} (${inst.email})${projs}\n`;
|
|
2146
|
+
}
|
|
2147
|
+
return { content: [{ type: "text", text }] };
|
|
2148
|
+
|
|
1854
2149
|
} else {
|
|
1855
2150
|
throw new Error(`Unknown tool: ${name}`);
|
|
1856
2151
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rui.branco/jira-mcp",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.10",
|
|
4
4
|
"description": "Jira MCP server for Claude Code - fetch tickets, search with JQL, update tickets, manage comments, change status, and get Figma designs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/setup.js
CHANGED
|
@@ -12,18 +12,92 @@ let args = process.argv.slice(2);
|
|
|
12
12
|
// Skip "setup" arg if called via index.js
|
|
13
13
|
if (args[0] === "setup") args = args.slice(1);
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// Load existing config if present
|
|
16
|
+
function loadConfig() {
|
|
17
|
+
try {
|
|
18
|
+
if (fs.existsSync(configPath)) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
18
24
|
|
|
25
|
+
function saveConfig(config) {
|
|
19
26
|
if (!fs.existsSync(configDir)) {
|
|
20
27
|
fs.mkdirSync(configDir, { recursive: true });
|
|
21
28
|
}
|
|
29
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Non-interactive: setup add <name> <email> <token> <baseUrl> <projects>
|
|
33
|
+
if (args[0] === "add" && args.length >= 5) {
|
|
34
|
+
const [, name, email, token, baseUrl, ...projectArgs] = args;
|
|
35
|
+
const projects = projectArgs.length > 0
|
|
36
|
+
? projectArgs.join(",").split(",").map((p) => p.trim().toUpperCase()).filter(Boolean)
|
|
37
|
+
: [];
|
|
38
|
+
|
|
39
|
+
const config = loadConfig() || {};
|
|
40
|
+
|
|
41
|
+
// Migrate old format if needed
|
|
42
|
+
if (config.email && !config.instances) {
|
|
43
|
+
config.instances = [{
|
|
44
|
+
name: "default",
|
|
45
|
+
email: config.email,
|
|
46
|
+
token: config.token,
|
|
47
|
+
baseUrl: config.baseUrl,
|
|
48
|
+
projects: [],
|
|
49
|
+
}];
|
|
50
|
+
config.defaultInstance = "default";
|
|
51
|
+
delete config.email;
|
|
52
|
+
delete config.token;
|
|
53
|
+
delete config.baseUrl;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!config.instances) config.instances = [];
|
|
57
|
+
|
|
58
|
+
// Replace or add instance
|
|
59
|
+
const existing = config.instances.findIndex((i) => i.name === name);
|
|
60
|
+
const instance = { name, email, token, baseUrl: baseUrl.replace(/\/$/, ""), projects };
|
|
61
|
+
if (existing >= 0) {
|
|
62
|
+
config.instances[existing] = instance;
|
|
63
|
+
} else {
|
|
64
|
+
config.instances.push(instance);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!config.defaultInstance) config.defaultInstance = name;
|
|
68
|
+
|
|
69
|
+
saveConfig(config);
|
|
70
|
+
console.log(`Instance "${name}" saved to ${configPath}`);
|
|
71
|
+
if (projects.length > 0) {
|
|
72
|
+
console.log(`Projects: ${projects.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Non-interactive: setup remove <name>
|
|
78
|
+
if (args[0] === "remove" && args.length >= 2) {
|
|
79
|
+
const name = args[1];
|
|
80
|
+
const config = loadConfig();
|
|
22
81
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
82
|
+
if (!config || !config.instances) {
|
|
83
|
+
console.error("No multi-instance config found.");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
config.instances = config.instances.filter((i) => i.name !== name);
|
|
88
|
+
if (config.defaultInstance === name) {
|
|
89
|
+
config.defaultInstance = config.instances[0]?.name || null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
saveConfig(config);
|
|
93
|
+
console.log(`Instance "${name}" removed.`);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Non-interactive: setup <email> <token> <baseUrl> (legacy single-instance)
|
|
98
|
+
if (args.length >= 3 && args[0] !== "add" && args[0] !== "remove") {
|
|
99
|
+
const [email, token, baseUrl] = args;
|
|
100
|
+
saveConfig({ email, token, baseUrl: baseUrl.replace(/\/$/, "") });
|
|
27
101
|
console.log(`Config saved to ${configPath}`);
|
|
28
102
|
process.exit(0);
|
|
29
103
|
}
|
|
@@ -41,7 +115,34 @@ function ask(question) {
|
|
|
41
115
|
}
|
|
42
116
|
|
|
43
117
|
async function setup() {
|
|
118
|
+
const existing = loadConfig();
|
|
119
|
+
const hasInstances = existing?.instances?.length > 0;
|
|
120
|
+
const isOldFormat = existing?.email && !existing?.instances;
|
|
121
|
+
|
|
44
122
|
console.log("\n=== Jira MCP Setup ===\n");
|
|
123
|
+
|
|
124
|
+
if (hasInstances || isOldFormat) {
|
|
125
|
+
if (isOldFormat) {
|
|
126
|
+
console.log(`Existing single-instance config found (${existing.baseUrl})\n`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log("Existing instances:");
|
|
129
|
+
for (const inst of existing.instances) {
|
|
130
|
+
const isDefault = inst.name === existing.defaultInstance ? " (default)" : "";
|
|
131
|
+
const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
|
|
132
|
+
console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}`);
|
|
133
|
+
}
|
|
134
|
+
console.log();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const action = await ask("Add new instance, or fresh setup? (add/fresh): ");
|
|
138
|
+
if (action.trim().toLowerCase() === "fresh") {
|
|
139
|
+
// Fall through to single setup
|
|
140
|
+
} else {
|
|
141
|
+
// Add instance to multi-instance config
|
|
142
|
+
return await addInstance(existing);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
45
146
|
console.log("To get your Jira API token:");
|
|
46
147
|
console.log("1. Go to https://id.atlassian.com/manage-profile/security/api-tokens");
|
|
47
148
|
console.log("2. Click 'Create API token'");
|
|
@@ -51,17 +152,85 @@ async function setup() {
|
|
|
51
152
|
const token = await ask("Jira API token: ");
|
|
52
153
|
const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
|
|
53
154
|
|
|
54
|
-
|
|
55
|
-
|
|
155
|
+
saveConfig({ email, token, baseUrl: baseUrl.replace(/\/$/, "") });
|
|
156
|
+
console.log(`\nConfig saved to ${configPath}`);
|
|
157
|
+
|
|
158
|
+
printFigmaStatus();
|
|
159
|
+
printSetupComplete();
|
|
160
|
+
rl.close();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function addInstance(config) {
|
|
164
|
+
// Migrate old format if needed
|
|
165
|
+
if (config.email && !config.instances) {
|
|
166
|
+
const oldName = await ask("Name for your existing instance (e.g., work): ");
|
|
167
|
+
const oldProjects = await ask("Project prefixes for existing instance (comma-separated, e.g., MODS,ENG): ");
|
|
168
|
+
config = {
|
|
169
|
+
instances: [{
|
|
170
|
+
name: oldName.trim() || "default",
|
|
171
|
+
email: config.email,
|
|
172
|
+
token: config.token,
|
|
173
|
+
baseUrl: config.baseUrl,
|
|
174
|
+
projects: oldProjects.split(",").map((p) => p.trim().toUpperCase()).filter(Boolean),
|
|
175
|
+
}],
|
|
176
|
+
defaultInstance: oldName.trim() || "default",
|
|
177
|
+
};
|
|
56
178
|
}
|
|
57
179
|
|
|
58
|
-
|
|
59
|
-
configPath,
|
|
60
|
-
JSON.stringify({ email, token, baseUrl: baseUrl.replace(/\/$/, "") }, null, 2)
|
|
61
|
-
);
|
|
62
|
-
console.log(`\nConfig saved to ${configPath}`);
|
|
180
|
+
if (!config.instances) config.instances = [];
|
|
63
181
|
|
|
64
|
-
|
|
182
|
+
console.log("\n--- Add New Instance ---\n");
|
|
183
|
+
console.log("To get your Jira API token:");
|
|
184
|
+
console.log("1. Go to https://id.atlassian.com/manage-profile/security/api-tokens");
|
|
185
|
+
console.log("2. Click 'Create API token'");
|
|
186
|
+
console.log("3. Copy the token\n");
|
|
187
|
+
|
|
188
|
+
const name = await ask("Instance name (e.g., personal): ");
|
|
189
|
+
const email = await ask("Jira email: ");
|
|
190
|
+
const token = await ask("Jira API token: ");
|
|
191
|
+
const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
|
|
192
|
+
const projectsInput = await ask("Project prefixes (comma-separated, e.g., SIDE,FUN): ");
|
|
193
|
+
const projects = projectsInput.split(",").map((p) => p.trim().toUpperCase()).filter(Boolean);
|
|
194
|
+
|
|
195
|
+
const instance = {
|
|
196
|
+
name: name.trim(),
|
|
197
|
+
email: email.trim(),
|
|
198
|
+
token: token.trim(),
|
|
199
|
+
baseUrl: baseUrl.trim().replace(/\/$/, ""),
|
|
200
|
+
projects,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// Replace or add
|
|
204
|
+
const idx = config.instances.findIndex((i) => i.name === instance.name);
|
|
205
|
+
if (idx >= 0) {
|
|
206
|
+
config.instances[idx] = instance;
|
|
207
|
+
} else {
|
|
208
|
+
config.instances.push(instance);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!config.defaultInstance) config.defaultInstance = instance.name;
|
|
212
|
+
|
|
213
|
+
const setDefault = await ask(`Set "${instance.name}" as default? (y/N): `);
|
|
214
|
+
if (setDefault.trim().toLowerCase() === "y") {
|
|
215
|
+
config.defaultInstance = instance.name;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
saveConfig(config);
|
|
219
|
+
console.log(`\nInstance "${instance.name}" saved to ${configPath}`);
|
|
220
|
+
|
|
221
|
+
console.log("\nAll instances:");
|
|
222
|
+
for (const inst of config.instances) {
|
|
223
|
+
const isDefault = inst.name === config.defaultInstance ? " (default)" : "";
|
|
224
|
+
const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
|
|
225
|
+
console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
printFigmaStatus();
|
|
229
|
+
printSetupComplete();
|
|
230
|
+
rl.close();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function printFigmaStatus() {
|
|
65
234
|
const figmaConfigPath = path.join(process.env.HOME, ".config/figma-mcp/config.json");
|
|
66
235
|
if (fs.existsSync(figmaConfigPath)) {
|
|
67
236
|
console.log("\n[OK] Figma MCP detected - Figma links in tickets will be fetched automatically");
|
|
@@ -69,13 +238,13 @@ async function setup() {
|
|
|
69
238
|
console.log("\n[INFO] Figma MCP not installed - Figma links won't be fetched");
|
|
70
239
|
console.log("To enable Figma integration, install figma-mcp");
|
|
71
240
|
}
|
|
241
|
+
}
|
|
72
242
|
|
|
243
|
+
function printSetupComplete() {
|
|
73
244
|
console.log("\n=== Setup Complete ===");
|
|
74
245
|
console.log("\nIf you haven't already, add to Claude Code with:\n");
|
|
75
246
|
console.log(" claude mcp add --transport stdio jira -- npx -y @rui.branco/jira-mcp");
|
|
76
247
|
console.log("\nThen restart Claude Code and run /mcp to verify.");
|
|
77
|
-
|
|
78
|
-
rl.close();
|
|
79
248
|
}
|
|
80
249
|
|
|
81
250
|
setup().catch((e) => {
|