@rui.branco/jira-mcp 1.6.8 → 1.6.9
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 +318 -52
- 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
|
},
|
|
@@ -1198,7 +1263,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1198
1263
|
{
|
|
1199
1264
|
name: "jira_search",
|
|
1200
1265
|
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'",
|
|
1266
|
+
"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
1267
|
inputSchema: {
|
|
1203
1268
|
type: "object",
|
|
1204
1269
|
properties: {
|
|
@@ -1213,6 +1278,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1213
1278
|
description:
|
|
1214
1279
|
"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
1280
|
},
|
|
1281
|
+
instance: {
|
|
1282
|
+
type: "string",
|
|
1283
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
1284
|
+
},
|
|
1216
1285
|
},
|
|
1217
1286
|
required: ["jql"],
|
|
1218
1287
|
},
|
|
@@ -1376,7 +1445,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1376
1445
|
{
|
|
1377
1446
|
name: "jira_search_users",
|
|
1378
1447
|
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.",
|
|
1448
|
+
"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
1449
|
inputSchema: {
|
|
1381
1450
|
type: "object",
|
|
1382
1451
|
properties: {
|
|
@@ -1389,6 +1458,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1389
1458
|
type: "number",
|
|
1390
1459
|
description: "Max results (default 5)",
|
|
1391
1460
|
},
|
|
1461
|
+
instance: {
|
|
1462
|
+
type: "string",
|
|
1463
|
+
description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
|
|
1464
|
+
},
|
|
1392
1465
|
},
|
|
1393
1466
|
required: ["query"],
|
|
1394
1467
|
},
|
|
@@ -1396,7 +1469,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1396
1469
|
{
|
|
1397
1470
|
name: "jira_get_changelog",
|
|
1398
1471
|
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.",
|
|
1472
|
+
"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
1473
|
inputSchema: {
|
|
1401
1474
|
type: "object",
|
|
1402
1475
|
properties: {
|
|
@@ -1415,10 +1488,75 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1415
1488
|
description:
|
|
1416
1489
|
"Max results when using jql (default 50). Each ticket requires a separate API call, so keep this reasonable.",
|
|
1417
1490
|
},
|
|
1491
|
+
instance: {
|
|
1492
|
+
type: "string",
|
|
1493
|
+
description: "Instance name (for multi-instance setups with jql). Auto-detected from issueKey if provided.",
|
|
1494
|
+
},
|
|
1418
1495
|
},
|
|
1419
1496
|
required: [],
|
|
1420
1497
|
},
|
|
1421
1498
|
},
|
|
1499
|
+
{
|
|
1500
|
+
name: "jira_add_instance",
|
|
1501
|
+
description:
|
|
1502
|
+
"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.",
|
|
1503
|
+
inputSchema: {
|
|
1504
|
+
type: "object",
|
|
1505
|
+
properties: {
|
|
1506
|
+
name: {
|
|
1507
|
+
type: "string",
|
|
1508
|
+
description: "Unique name for this instance (e.g., 'work', 'personal')",
|
|
1509
|
+
},
|
|
1510
|
+
email: {
|
|
1511
|
+
type: "string",
|
|
1512
|
+
description: "Jira account email",
|
|
1513
|
+
},
|
|
1514
|
+
token: {
|
|
1515
|
+
type: "string",
|
|
1516
|
+
description: "Jira API token (from https://id.atlassian.com/manage-profile/security/api-tokens)",
|
|
1517
|
+
},
|
|
1518
|
+
baseUrl: {
|
|
1519
|
+
type: "string",
|
|
1520
|
+
description: "Jira base URL (e.g., https://company.atlassian.net)",
|
|
1521
|
+
},
|
|
1522
|
+
projects: {
|
|
1523
|
+
type: "array",
|
|
1524
|
+
items: { type: "string" },
|
|
1525
|
+
description: "Project key prefixes to auto-route to this instance (e.g., ['PROJ', 'ENG'])",
|
|
1526
|
+
},
|
|
1527
|
+
setDefault: {
|
|
1528
|
+
type: "boolean",
|
|
1529
|
+
description: "Set this instance as the default (default: false)",
|
|
1530
|
+
},
|
|
1531
|
+
},
|
|
1532
|
+
required: ["name", "email", "token", "baseUrl"],
|
|
1533
|
+
},
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
name: "jira_remove_instance",
|
|
1537
|
+
description:
|
|
1538
|
+
"Remove a Jira instance configuration by name. Cannot remove the last remaining instance.",
|
|
1539
|
+
inputSchema: {
|
|
1540
|
+
type: "object",
|
|
1541
|
+
properties: {
|
|
1542
|
+
name: {
|
|
1543
|
+
type: "string",
|
|
1544
|
+
description: "Name of the instance to remove",
|
|
1545
|
+
},
|
|
1546
|
+
},
|
|
1547
|
+
required: ["name"],
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
name: "jira_list_instances",
|
|
1552
|
+
description:
|
|
1553
|
+
"List all configured Jira instances with their names, URLs, project prefixes, and which is the default.",
|
|
1554
|
+
inputSchema: {
|
|
1555
|
+
type: "object",
|
|
1556
|
+
properties: {},
|
|
1557
|
+
required: [],
|
|
1558
|
+
},
|
|
1559
|
+
},
|
|
1422
1560
|
],
|
|
1423
1561
|
};
|
|
1424
1562
|
});
|
|
@@ -1428,7 +1566,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1428
1566
|
|
|
1429
1567
|
try {
|
|
1430
1568
|
if (name === "jira_get_myself") {
|
|
1431
|
-
const
|
|
1569
|
+
const inst = getInstanceByName(args.instance);
|
|
1570
|
+
const result = await fetchJira("/myself", {}, inst);
|
|
1432
1571
|
return {
|
|
1433
1572
|
content: [
|
|
1434
1573
|
{
|
|
@@ -1438,9 +1577,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1438
1577
|
],
|
|
1439
1578
|
};
|
|
1440
1579
|
} else if (name === "jira_search_users") {
|
|
1580
|
+
const inst = getInstanceByName(args.instance);
|
|
1441
1581
|
const maxResults = args.maxResults || 5;
|
|
1442
1582
|
const users = await fetchJira(
|
|
1443
1583
|
`/user/search?query=${encodeURIComponent(args.query)}&maxResults=${maxResults}`,
|
|
1584
|
+
{},
|
|
1585
|
+
inst,
|
|
1444
1586
|
);
|
|
1445
1587
|
if (!users || users.length === 0) {
|
|
1446
1588
|
return {
|
|
@@ -1511,11 +1653,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1511
1653
|
|
|
1512
1654
|
return { content };
|
|
1513
1655
|
} else if (name === "jira_search") {
|
|
1514
|
-
const
|
|
1656
|
+
const inst = getInstanceByName(args.instance);
|
|
1657
|
+
const result = await searchTickets(args.jql, args.maxResults || 10, args.fields || null, inst);
|
|
1515
1658
|
return { content: [{ type: "text", text: result }] };
|
|
1516
1659
|
} else if (name === "jira_add_comment") {
|
|
1660
|
+
const inst = getInstanceForKey(args.issueKey);
|
|
1517
1661
|
// Build ADF content with mention support
|
|
1518
|
-
const adfContent = await buildCommentADF(args.comment);
|
|
1662
|
+
const adfContent = await buildCommentADF(args.comment, inst);
|
|
1519
1663
|
const body = {
|
|
1520
1664
|
body: {
|
|
1521
1665
|
version: 1,
|
|
@@ -1526,7 +1670,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1526
1670
|
const result = await fetchJira(`/issue/${args.issueKey}/comment`, {
|
|
1527
1671
|
method: "POST",
|
|
1528
1672
|
body,
|
|
1529
|
-
});
|
|
1673
|
+
}, inst);
|
|
1530
1674
|
const author = result.author?.displayName || "Unknown";
|
|
1531
1675
|
const created = new Date(result.created).toLocaleString();
|
|
1532
1676
|
return {
|
|
@@ -1538,9 +1682,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1538
1682
|
],
|
|
1539
1683
|
};
|
|
1540
1684
|
} else if (name === "jira_reply_comment") {
|
|
1685
|
+
const inst = getInstanceForKey(args.issueKey);
|
|
1541
1686
|
// Fetch the original comment
|
|
1542
1687
|
const original = await fetchJira(
|
|
1543
1688
|
`/issue/${args.issueKey}/comment/${args.commentId}`,
|
|
1689
|
+
{},
|
|
1690
|
+
inst,
|
|
1544
1691
|
);
|
|
1545
1692
|
const originalAuthor = original.author?.displayName || "Unknown";
|
|
1546
1693
|
const originalAccountId = original.author?.accountId;
|
|
@@ -1591,7 +1738,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1591
1738
|
const result = await fetchJira(`/issue/${args.issueKey}/comment`, {
|
|
1592
1739
|
method: "POST",
|
|
1593
1740
|
body,
|
|
1594
|
-
});
|
|
1741
|
+
}, inst);
|
|
1595
1742
|
const author = result.author?.displayName || "Unknown";
|
|
1596
1743
|
const created = new Date(result.created).toLocaleString();
|
|
1597
1744
|
return {
|
|
@@ -1603,8 +1750,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1603
1750
|
],
|
|
1604
1751
|
};
|
|
1605
1752
|
} else if (name === "jira_edit_comment") {
|
|
1753
|
+
const inst = getInstanceForKey(args.issueKey);
|
|
1606
1754
|
// Build ADF content with mention support
|
|
1607
|
-
const adfContent = await buildCommentADF(args.comment);
|
|
1755
|
+
const adfContent = await buildCommentADF(args.comment, inst);
|
|
1608
1756
|
const body = {
|
|
1609
1757
|
body: {
|
|
1610
1758
|
version: 1,
|
|
@@ -1615,6 +1763,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1615
1763
|
const result = await fetchJira(
|
|
1616
1764
|
`/issue/${args.issueKey}/comment/${args.commentId}`,
|
|
1617
1765
|
{ method: "PUT", body },
|
|
1766
|
+
inst,
|
|
1618
1767
|
);
|
|
1619
1768
|
return {
|
|
1620
1769
|
content: [
|
|
@@ -1625,9 +1774,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1625
1774
|
],
|
|
1626
1775
|
};
|
|
1627
1776
|
} else if (name === "jira_delete_comment") {
|
|
1777
|
+
const inst = getInstanceForKey(args.issueKey);
|
|
1628
1778
|
await fetchJira(`/issue/${args.issueKey}/comment/${args.commentId}`, {
|
|
1629
1779
|
method: "DELETE",
|
|
1630
|
-
});
|
|
1780
|
+
}, inst);
|
|
1631
1781
|
return {
|
|
1632
1782
|
content: [
|
|
1633
1783
|
{
|
|
@@ -1637,9 +1787,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1637
1787
|
],
|
|
1638
1788
|
};
|
|
1639
1789
|
} else if (name === "jira_transition") {
|
|
1790
|
+
const inst = getInstanceForKey(args.issueKey);
|
|
1640
1791
|
if (!args.transitionId && !args.targetStatus) {
|
|
1641
1792
|
// List available transitions
|
|
1642
|
-
const result = await fetchJira(`/issue/${args.issueKey}/transitions
|
|
1793
|
+
const result = await fetchJira(`/issue/${args.issueKey}/transitions`, {}, inst);
|
|
1643
1794
|
let output = `# Available transitions for ${args.issueKey}\n\n`;
|
|
1644
1795
|
for (const t of result.transitions || []) {
|
|
1645
1796
|
output += `- **${t.name}** (id: ${t.id}) → status: ${t.to?.name || "Unknown"}\n`;
|
|
@@ -1657,7 +1808,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1657
1808
|
|
|
1658
1809
|
// Try to reach target status, with up to 3 intermediate transitions
|
|
1659
1810
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1660
|
-
const result = await fetchJira(`/issue/${args.issueKey}/transitions
|
|
1811
|
+
const result = await fetchJira(`/issue/${args.issueKey}/transitions`, {}, inst);
|
|
1661
1812
|
const available = result.transitions || [];
|
|
1662
1813
|
|
|
1663
1814
|
// Check if target status is directly available
|
|
@@ -1671,7 +1822,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1671
1822
|
await fetchJira(`/issue/${args.issueKey}/transitions`, {
|
|
1672
1823
|
method: "POST",
|
|
1673
1824
|
body: { transition: { id: directMatch.id } },
|
|
1674
|
-
});
|
|
1825
|
+
}, inst);
|
|
1675
1826
|
transitions.push(directMatch.to?.name || directMatch.name);
|
|
1676
1827
|
return {
|
|
1677
1828
|
content: [
|
|
@@ -1694,7 +1845,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1694
1845
|
await fetchJira(`/issue/${args.issueKey}/transitions`, {
|
|
1695
1846
|
method: "POST",
|
|
1696
1847
|
body: { transition: { id: inProgress.id } },
|
|
1697
|
-
});
|
|
1848
|
+
}, inst);
|
|
1698
1849
|
transitions.push(inProgress.to?.name || "In Progress");
|
|
1699
1850
|
continue; // Try again to find target
|
|
1700
1851
|
}
|
|
@@ -1704,7 +1855,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1704
1855
|
}
|
|
1705
1856
|
|
|
1706
1857
|
// Could not reach target status
|
|
1707
|
-
const result = await fetchJira(`/issue/${args.issueKey}/transitions
|
|
1858
|
+
const result = await fetchJira(`/issue/${args.issueKey}/transitions`, {}, inst);
|
|
1708
1859
|
const availableNames = (result.transitions || [])
|
|
1709
1860
|
.map((t) => t.to?.name || t.name)
|
|
1710
1861
|
.join(", ");
|
|
@@ -1722,7 +1873,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1722
1873
|
await fetchJira(`/issue/${args.issueKey}/transitions`, {
|
|
1723
1874
|
method: "POST",
|
|
1724
1875
|
body: { transition: { id: args.transitionId } },
|
|
1725
|
-
});
|
|
1876
|
+
}, inst);
|
|
1726
1877
|
return {
|
|
1727
1878
|
content: [
|
|
1728
1879
|
{
|
|
@@ -1732,6 +1883,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1732
1883
|
],
|
|
1733
1884
|
};
|
|
1734
1885
|
} else if (name === "jira_update_ticket") {
|
|
1886
|
+
const inst = getInstanceForKey(args.issueKey);
|
|
1735
1887
|
const fields = {};
|
|
1736
1888
|
if (args.summary) {
|
|
1737
1889
|
if (args.replaceSummary) {
|
|
@@ -1739,7 +1891,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1739
1891
|
} else {
|
|
1740
1892
|
// Append to existing title (default)
|
|
1741
1893
|
const issue = await fetchJira(
|
|
1742
|
-
`/issue/${args.issueKey}?fields=summary`,
|
|
1894
|
+
`/issue/${args.issueKey}?fields=summary`, {}, inst,
|
|
1743
1895
|
);
|
|
1744
1896
|
const existing = issue.fields?.summary || "";
|
|
1745
1897
|
fields.summary = existing + " " + args.summary;
|
|
@@ -1760,7 +1912,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1760
1912
|
} else {
|
|
1761
1913
|
// Append to existing (default)
|
|
1762
1914
|
const issue = await fetchJira(
|
|
1763
|
-
`/issue/${args.issueKey}?fields=description`,
|
|
1915
|
+
`/issue/${args.issueKey}?fields=description`, {}, inst,
|
|
1764
1916
|
);
|
|
1765
1917
|
const existing = issue.fields?.description;
|
|
1766
1918
|
if (existing && existing.content) {
|
|
@@ -1777,7 +1929,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1777
1929
|
}
|
|
1778
1930
|
if (args.removeFromDescription) {
|
|
1779
1931
|
const issue = await fetchJira(
|
|
1780
|
-
`/issue/${args.issueKey}?fields=description`,
|
|
1932
|
+
`/issue/${args.issueKey}?fields=description`, {}, inst,
|
|
1781
1933
|
);
|
|
1782
1934
|
const existing = issue.fields?.description;
|
|
1783
1935
|
if (existing && existing.content) {
|
|
@@ -1830,7 +1982,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1830
1982
|
await fetchJira(`/issue/${args.issueKey}`, {
|
|
1831
1983
|
method: "PUT",
|
|
1832
1984
|
body: { fields },
|
|
1833
|
-
});
|
|
1985
|
+
}, inst);
|
|
1834
1986
|
const updated = Object.keys(fields).join(", ");
|
|
1835
1987
|
return {
|
|
1836
1988
|
content: [
|
|
@@ -1848,9 +2000,123 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1848
2000
|
const result = await getChangelog(args.issueKey);
|
|
1849
2001
|
return { content: [{ type: "text", text: result.formatted }] };
|
|
1850
2002
|
} else {
|
|
1851
|
-
const
|
|
2003
|
+
const inst = getInstanceByName(args.instance);
|
|
2004
|
+
const result = await getChangelogsBulk(args.jql, args.maxResults || 50, inst);
|
|
1852
2005
|
return { content: [{ type: "text", text: result }] };
|
|
1853
2006
|
}
|
|
2007
|
+
} else if (name === "jira_add_instance") {
|
|
2008
|
+
const instName = args.name.trim();
|
|
2009
|
+
const projects = (args.projects || []).map((p) => p.toUpperCase());
|
|
2010
|
+
const authStr = Buffer.from(`${args.email}:${args.token}`).toString("base64");
|
|
2011
|
+
const newInstance = {
|
|
2012
|
+
name: instName,
|
|
2013
|
+
email: args.email,
|
|
2014
|
+
token: args.token,
|
|
2015
|
+
baseUrl: args.baseUrl.replace(/\/$/, ""),
|
|
2016
|
+
projects,
|
|
2017
|
+
auth: authStr,
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
// Update in-memory instances
|
|
2021
|
+
const existingIdx = instances.findIndex((i) => i.name === instName);
|
|
2022
|
+
if (existingIdx >= 0) {
|
|
2023
|
+
instances[existingIdx] = newInstance;
|
|
2024
|
+
} else {
|
|
2025
|
+
instances.push(newInstance);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// Update default if requested or if it's the first instance
|
|
2029
|
+
if (args.setDefault || instances.length === 1) {
|
|
2030
|
+
// Can't reassign const, but defaultInstance is used via getInstanceByName/getInstanceForKey
|
|
2031
|
+
// which search the instances array, so this is handled by rawConfig.defaultInstance below
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Persist to config file
|
|
2035
|
+
const savedConfig = loadConfigFile();
|
|
2036
|
+
if (!savedConfig.instances) {
|
|
2037
|
+
// Migrate old format
|
|
2038
|
+
if (savedConfig.email) {
|
|
2039
|
+
savedConfig.instances = [{
|
|
2040
|
+
name: "default",
|
|
2041
|
+
email: savedConfig.email,
|
|
2042
|
+
token: savedConfig.token,
|
|
2043
|
+
baseUrl: savedConfig.baseUrl,
|
|
2044
|
+
projects: [],
|
|
2045
|
+
}];
|
|
2046
|
+
savedConfig.defaultInstance = "default";
|
|
2047
|
+
delete savedConfig.email;
|
|
2048
|
+
delete savedConfig.token;
|
|
2049
|
+
delete savedConfig.baseUrl;
|
|
2050
|
+
} else {
|
|
2051
|
+
savedConfig.instances = [];
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// Save without the computed auth field
|
|
2056
|
+
const toSave = { name: instName, email: args.email, token: args.token, baseUrl: newInstance.baseUrl, projects };
|
|
2057
|
+
const savedIdx = savedConfig.instances.findIndex((i) => i.name === instName);
|
|
2058
|
+
if (savedIdx >= 0) {
|
|
2059
|
+
savedConfig.instances[savedIdx] = toSave;
|
|
2060
|
+
} else {
|
|
2061
|
+
savedConfig.instances.push(toSave);
|
|
2062
|
+
}
|
|
2063
|
+
if (args.setDefault || !savedConfig.defaultInstance) {
|
|
2064
|
+
savedConfig.defaultInstance = instName;
|
|
2065
|
+
}
|
|
2066
|
+
fs.writeFileSync(jiraConfigPath, JSON.stringify(savedConfig, null, 2));
|
|
2067
|
+
|
|
2068
|
+
const action = existingIdx >= 0 ? "Updated" : "Added";
|
|
2069
|
+
let text = `${action} instance "${instName}" (${newInstance.baseUrl}).`;
|
|
2070
|
+
if (projects.length > 0) text += ` Projects: ${projects.join(", ")}.`;
|
|
2071
|
+
if (args.setDefault) text += " Set as default.";
|
|
2072
|
+
|
|
2073
|
+
return { content: [{ type: "text", text }] };
|
|
2074
|
+
|
|
2075
|
+
} else if (name === "jira_remove_instance") {
|
|
2076
|
+
const instName = args.name.trim();
|
|
2077
|
+
|
|
2078
|
+
if (instances.length <= 1) {
|
|
2079
|
+
return {
|
|
2080
|
+
content: [{ type: "text", text: "Cannot remove the last remaining instance." }],
|
|
2081
|
+
isError: true,
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
const idx = instances.findIndex((i) => i.name === instName);
|
|
2086
|
+
if (idx < 0) {
|
|
2087
|
+
return {
|
|
2088
|
+
content: [{ type: "text", text: `Instance "${instName}" not found.` }],
|
|
2089
|
+
isError: true,
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
instances.splice(idx, 1);
|
|
2094
|
+
|
|
2095
|
+
// Persist to config file
|
|
2096
|
+
const savedConfig = loadConfigFile();
|
|
2097
|
+
if (savedConfig.instances) {
|
|
2098
|
+
savedConfig.instances = savedConfig.instances.filter((i) => i.name !== instName);
|
|
2099
|
+
if (savedConfig.defaultInstance === instName) {
|
|
2100
|
+
savedConfig.defaultInstance = savedConfig.instances[0]?.name || null;
|
|
2101
|
+
}
|
|
2102
|
+
fs.writeFileSync(jiraConfigPath, JSON.stringify(savedConfig, null, 2));
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
return { content: [{ type: "text", text: `Removed instance "${instName}".` }] };
|
|
2106
|
+
|
|
2107
|
+
} else if (name === "jira_list_instances") {
|
|
2108
|
+
if (instances.length === 0) {
|
|
2109
|
+
return { content: [{ type: "text", text: "No instances configured." }] };
|
|
2110
|
+
}
|
|
2111
|
+
const currentDefault = rawConfig.defaultInstance || instances[0].name;
|
|
2112
|
+
let text = `# Configured Jira Instances (${instances.length})\n\n`;
|
|
2113
|
+
for (const inst of instances) {
|
|
2114
|
+
const isDefault = inst.name === currentDefault ? " **(default)**" : "";
|
|
2115
|
+
const projs = inst.projects?.length > 0 ? `\n Projects: ${inst.projects.join(", ")}` : "";
|
|
2116
|
+
text += `- **${inst.name}**${isDefault}: ${inst.baseUrl} (${inst.email})${projs}\n`;
|
|
2117
|
+
}
|
|
2118
|
+
return { content: [{ type: "text", text }] };
|
|
2119
|
+
|
|
1854
2120
|
} else {
|
|
1855
2121
|
throw new Error(`Unknown tool: ${name}`);
|
|
1856
2122
|
}
|
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.9",
|
|
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) => {
|