@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.
Files changed (3) hide show
  1. package/index.js +348 -53
  2. package/package.json +1 -1
  3. 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 jiraConfig = JSON.parse(fs.readFileSync(jiraConfigPath, "utf8"));
55
- const auth = Buffer.from(`${jiraConfig.email}:${jiraConfig.token}`).toString(
56
- "base64",
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(`${jiraConfig.baseUrl}/rest/api/3${endpoint}`, {
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
- const issue = await fetchJira(`/issue/${issueKey}?expand=renderedFields`);
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
- const issue = await fetchJira(`/issue/${issueKey}?expand=changelog&fields=created`);
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 result = await fetchJira("/myself");
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 result = await getTicket(args.issueKey, downloadImages, fetchFigma);
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 result = await searchTickets(args.jql, args.maxResults || 10, args.fields || null);
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 result = await getChangelogsBulk(args.jql, args.maxResults || 50);
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.8",
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
- if (args.length >= 3) {
16
- // Non-interactive mode: node setup.js <email> <token> <baseUrl>
17
- const [email, token, baseUrl] = args;
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
- fs.writeFileSync(
24
- configPath,
25
- JSON.stringify({ email, token, baseUrl: baseUrl.replace(/\/$/, "") }, null, 2)
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
- if (!fs.existsSync(configDir)) {
55
- fs.mkdirSync(configDir, { recursive: true });
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
- fs.writeFileSync(
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
- // Check for Figma MCP
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) => {