@rui.branco/jira-mcp 1.7.12 → 1.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -136,6 +136,8 @@ Use `@FirstName LastName` syntax to mention users in comments:
136
136
  # The mention is automatically resolved and the user gets notified
137
137
  ```
138
138
 
139
+ Mentions are resolved with Jira's global user search first. On instances where your account lacks the global "Browse users and groups" permission (the search comes back empty), the name is looked up among users assignable to the ticket being commented on (`/user/assignable/search?issueKey=...`), or, when no ticket is known, among users assignable in any of the instance's configured `projects` (one `/user/assignable/search?project=...` request per project, results merged and deduplicated; a project you can't access is skipped). A name that still can't be resolved is posted as plain text, which notifies nobody. `jira_search_users` uses the same configured-projects fallback when you don't pass `issueKey` or `projectKey`.
140
+
139
141
  ### Example Output
140
142
 
141
143
  ```
@@ -250,6 +252,43 @@ Config stored at `~/.config/jira-mcp/config.json`:
250
252
  }
251
253
  ```
252
254
 
255
+ #### Multiple instances
256
+
257
+ ```json
258
+ {
259
+ "instances": [
260
+ {
261
+ "name": "work",
262
+ "email": "you@company.com",
263
+ "token": "...",
264
+ "baseUrl": "https://company.atlassian.net",
265
+ "projects": ["PROJ", "ENG"]
266
+ },
267
+ {
268
+ "name": "client",
269
+ "email": "you@company.com",
270
+ "token": "...",
271
+ "baseUrl": "https://client.atlassian.net",
272
+ "projects": ["CLI"],
273
+ "paths": ["~/code/client-app", "/Users/you/code/client-api"]
274
+ }
275
+ ],
276
+ "defaultInstance": "work"
277
+ }
278
+ ```
279
+
280
+ - `projects`: project key prefixes routed to the instance. Tools that receive an issue or project key (e.g. `CLI-12`) are routed to the instance that owns the prefix.
281
+ - `paths` (optional): project directories for which this instance is the default. The MCP server runs with your project directory as its working directory. If that directory is equal to or inside one of these paths, the instance becomes this session's default (the longest matching path wins). Paths must be absolute, and `~` expands to your home directory.
282
+ - `defaultInstance`: global fallback used when neither `JIRA_MCP_INSTANCE` nor a `paths` entry applies. The per-session choice never overwrites it.
283
+
284
+ The session default is used whenever a call has no issue or project key to route by, for example `jira_search`, `jira_search_users` without a key, or `jira_get_myself`. It is resolved at startup in this order:
285
+
286
+ 1. `JIRA_MCP_INSTANCE=<name>` environment variable (ignored with a warning if no instance has that name).
287
+ 2. The instance whose `paths` contain the working directory.
288
+ 3. `defaultInstance` from the config (or the first instance if it's unset).
289
+
290
+ `jira_list_instances` shows the current session default and where it came from (env, path, or config). `jira_add_instance` with `setDefault: true` changes `defaultInstance` in the config, but the session keeps its env or path choice when one applies.
291
+
253
292
  ## Error Handling
254
293
 
255
294
  The server provides clear error messages:
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000000"><path d="M11.571 11.513H0a5.218 5.218 0 0 0 5.232 5.215h2.13v2.057A5.215 5.215 0 0 0 12.575 24V12.518a1.005 1.005 0 0 0-1.005-1.005zm5.723-5.756H5.736a5.215 5.215 0 0 0 5.215 5.214h2.129v2.058a5.218 5.218 0 0 0 5.215 5.214V6.758a1.001 1.001 0 0 0-1.001-1.001zM23.013 0H11.455a5.215 5.215 0 0 0 5.215 5.215h2.129v2.057A5.215 5.215 0 0 0 24 12.483V1.005A1.001 1.001 0 0 0 23.013 0Z"/></svg>
package/icons.js ADDED
@@ -0,0 +1,22 @@
1
+ // serverInfo.icons for the MCP handshake: one single-colour silhouette sent
2
+ // twice, black for light backgrounds and white for dark ones.
3
+ // assets/icon.svg is the Jira logo from Simple Icons v13.21.0
4
+ // (https://simpleicons.org), CC0-1.0.
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const SVG = fs.readFileSync(path.join(__dirname, "assets", "icon.svg"), "utf8").trim();
9
+
10
+ function icon(fill, theme) {
11
+ const svg = SVG.replace('fill="#000000"', `fill="${fill}"`);
12
+ return {
13
+ src: `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`,
14
+ mimeType: "image/svg+xml",
15
+ sizes: ["any"],
16
+ theme,
17
+ };
18
+ }
19
+
20
+ const ICONS = [icon("#000000", "light"), icon("#FFFFFF", "dark")];
21
+
22
+ module.exports = { ICONS };
package/index.js CHANGED
@@ -19,6 +19,7 @@ const os = require("os");
19
19
  const path = require("path");
20
20
  const fetch = require("node-fetch");
21
21
  const configStore = require("./config-store.js");
22
+ const { ICONS } = require("./icons.js");
22
23
  const { spawn } = require("child_process");
23
24
 
24
25
  const PKG_VERSION = require("./package.json").version;
@@ -106,10 +107,89 @@ if (Array.isArray(rawConfig.instances)) {
106
107
  instances = [legacyInstance];
107
108
  }
108
109
 
109
- // Resolve default instance
110
- let defaultInstance =
111
- (rawConfig.defaultInstance && instances.find((i) => i.name === rawConfig.defaultInstance)) ||
112
- instances[0];
110
+ // Resolve default instance. The config `defaultInstance` is the global
111
+ // fallback; this process's session default (used for routing fallbacks and
112
+ // fetch defaults) is picked by env JIRA_MCP_INSTANCE, then by the longest
113
+ // instance `paths` entry containing the working directory, then the config.
114
+ function realpathOrSelf(value) {
115
+ try {
116
+ return fs.realpathSync(value);
117
+ } catch {
118
+ return value;
119
+ }
120
+ }
121
+
122
+ function expandConfiguredPath(value) {
123
+ if (typeof value !== "string" || !value.trim()) return null;
124
+ let expanded = value.trim();
125
+ if (expanded === "~" || expanded.startsWith("~/")) {
126
+ expanded = path.join(process.env.HOME || "", expanded.slice(1));
127
+ }
128
+ if (!path.isAbsolute(expanded)) return null;
129
+ return realpathOrSelf(path.resolve(expanded));
130
+ }
131
+
132
+ function isSameOrInsidePath(dir, parent) {
133
+ const relative = path.relative(parent, dir);
134
+ return relative === "" ||
135
+ (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
136
+ }
137
+
138
+ function resolveSessionDefault(instanceList, configDefaultName, cwd, env) {
139
+ const configDefault =
140
+ (configDefaultName && instanceList.find((i) => i.name === configDefaultName)) ||
141
+ instanceList[0];
142
+ const envName = typeof env.JIRA_MCP_INSTANCE === "string" ? env.JIRA_MCP_INSTANCE.trim() : "";
143
+ if (envName) {
144
+ const envInstance = instanceList.find((i) => i.name === envName);
145
+ if (envInstance) return { instance: envInstance, source: "env" };
146
+ }
147
+ const ignoredEnv = envName || undefined;
148
+
149
+ let best = null;
150
+ if (cwd) {
151
+ const realCwd = realpathOrSelf(path.resolve(cwd));
152
+ for (const instance of instanceList) {
153
+ for (const configured of Array.isArray(instance.paths) ? instance.paths : []) {
154
+ const resolved = expandConfiguredPath(configured);
155
+ if (
156
+ resolved &&
157
+ isSameOrInsidePath(realCwd, resolved) &&
158
+ (!best || resolved.length > best.path.length)
159
+ ) {
160
+ best = { instance, source: "path", path: resolved, ignoredEnv };
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return best || { instance: configDefault, source: "config", ignoredEnv };
166
+ }
167
+
168
+ let sessionCwd = null;
169
+ try {
170
+ sessionCwd = process.cwd();
171
+ } catch {
172
+ // Working directory was removed; fall back to env/config only.
173
+ }
174
+
175
+ let defaultInstance;
176
+ let sessionDefault;
177
+ function applySessionDefault() {
178
+ sessionDefault = resolveSessionDefault(instances, rawConfig.defaultInstance, sessionCwd, process.env);
179
+ defaultInstance = sessionDefault.instance;
180
+ }
181
+ applySessionDefault();
182
+ if (sessionDefault.ignoredEnv) {
183
+ console.error(`jira-mcp: JIRA_MCP_INSTANCE "${sessionDefault.ignoredEnv}" does not match a configured instance, ignoring it`);
184
+ }
185
+
186
+ function describeSessionDefault() {
187
+ if (sessionDefault.source === "env") return "env JIRA_MCP_INSTANCE";
188
+ if (sessionDefault.source === "path") {
189
+ return `path ${sessionDefault.path} (working directory ${sessionCwd})`;
190
+ }
191
+ return "config defaultInstance";
192
+ }
113
193
 
114
194
  // Instance resolution helpers
115
195
  function getMappedInstancesForProject(projectPrefix) {
@@ -1117,7 +1197,35 @@ const ECHO_DIRECTIVE =
1117
1197
  // Cache for user lookups to avoid repeated API calls
1118
1198
  const userCache = new Map();
1119
1199
 
1120
- async function searchUser(query, instance = defaultInstance) {
1200
+ // Users assignable in any of the instance's configured projects. Visible even
1201
+ // when the account lacks the global "Browse users and groups" permission.
1202
+ // One request per project because /user/assignable/multiProjectSearch returns
1203
+ // the intersection (users assignable in ALL listed projects), not the union.
1204
+ // Results are merged in config order, deduped by accountId; a project that
1205
+ // errors (e.g. 403/404) is skipped.
1206
+ async function searchAssignableUsersInProjects(instance, query, maxResults) {
1207
+ const results = await Promise.all(
1208
+ (instance.projects || []).map((project) =>
1209
+ fetchJira(
1210
+ `/user/assignable/search?project=${encodeURIComponent(project)}&query=${encodeURIComponent(query)}&maxResults=${maxResults}`,
1211
+ {},
1212
+ instance,
1213
+ ).catch(() => []),
1214
+ ),
1215
+ );
1216
+ const users = [];
1217
+ const seen = new Set();
1218
+ for (const projectUsers of results) {
1219
+ for (const user of Array.isArray(projectUsers) ? projectUsers : []) {
1220
+ if (seen.has(user.accountId)) continue;
1221
+ seen.add(user.accountId);
1222
+ users.push(user);
1223
+ }
1224
+ }
1225
+ return users.slice(0, maxResults);
1226
+ }
1227
+
1228
+ async function searchUser(query, instance = defaultInstance, issueKey = null) {
1121
1229
  // Check cache first (instance-aware)
1122
1230
  const cacheKey = `${instance.name}:${query.toLowerCase()}`;
1123
1231
  if (userCache.has(cacheKey)) {
@@ -1126,11 +1234,22 @@ async function searchUser(query, instance = defaultInstance) {
1126
1234
 
1127
1235
  try {
1128
1236
  // Search for users by display name
1129
- const users = await fetchJira(
1237
+ let users = await fetchJira(
1130
1238
  `/user/search?query=${encodeURIComponent(query)}&maxResults=5`,
1131
1239
  {},
1132
1240
  instance,
1133
- );
1241
+ ).catch(() => []);
1242
+ if (!Array.isArray(users) || users.length === 0) {
1243
+ // Global search is empty without "Browse users and groups" — fall back
1244
+ // to users assignable to the issue, else to the instance's projects.
1245
+ users = issueKey
1246
+ ? await fetchJira(
1247
+ `/user/assignable/search?issueKey=${encodeURIComponent(issueKey)}&query=${encodeURIComponent(query)}&maxResults=5`,
1248
+ {},
1249
+ instance,
1250
+ )
1251
+ : await searchAssignableUsersInProjects(instance, query, 5);
1252
+ }
1134
1253
  if (users && users.length > 0) {
1135
1254
  // Find best match - prefer exact match, then starts with, then contains
1136
1255
  const exactMatch = users.find(
@@ -1156,7 +1275,7 @@ async function searchUser(query, instance = defaultInstance) {
1156
1275
 
1157
1276
  // Parse text with @mentions and build ADF content
1158
1277
  // Parse inline formatting: **bold**, *italic*, `code`, [links](url), @mentions
1159
- async function parseInlineFormatting(text, instance = defaultInstance) {
1278
+ async function parseInlineFormatting(text, instance = defaultInstance, issueKey = null) {
1160
1279
  const nodes = [];
1161
1280
  // Links, bold, italic, inline code, mentions — links must come first to avoid ** inside link text being parsed as bold
1162
1281
  const regex = /(\[([^\]]+)\]\(([^)]+)\)|`(.+?)`|\*\*(.+?)\*\*|\*(.+?)\*|~~(.+?)~~|@([A-Z][a-zA-Zà-ÿ]*(?:\s[A-Z][a-zA-Zà-ÿ]*)*))/g;
@@ -1192,7 +1311,7 @@ async function parseInlineFormatting(text, instance = defaultInstance) {
1192
1311
  // @Mention — regex may greedily capture extra capitalized words beyond the actual name.
1193
1312
  // After resolving, only consume the portion matching the display name.
1194
1313
  const captured = match[8].trim();
1195
- const user = await searchUser(captured, instance);
1314
+ const user = await searchUser(captured, instance, issueKey);
1196
1315
  if (user) {
1197
1316
  nodes.push({
1198
1317
  type: "mention",
@@ -1302,7 +1421,7 @@ function listItemMatch(line) {
1302
1421
  // Recursively parse a (possibly nested) markdown list starting at lines[startIdx].
1303
1422
  // baseIndent is the indentation of items at this level; more-indented items are
1304
1423
  // folded into the previous item as a nested list. Returns {node, nextIndex}.
1305
- async function parseListBlock(lines, startIdx, baseIndent, instance) {
1424
+ async function parseListBlock(lines, startIdx, baseIndent, instance, issueKey = null) {
1306
1425
  const items = [];
1307
1426
  let i = startIdx;
1308
1427
  let ordered = null;
@@ -1313,7 +1432,7 @@ async function parseListBlock(lines, startIdx, baseIndent, instance) {
1313
1432
  if (m.indent < baseIndent) break; // dedent: belongs to a parent list
1314
1433
  if (m.indent > baseIndent) {
1315
1434
  // Deeper indent: nest under the previous item at this level.
1316
- const sub = await parseListBlock(lines, i, m.indent, instance);
1435
+ const sub = await parseListBlock(lines, i, m.indent, instance, issueKey);
1317
1436
  if (items.length > 0) {
1318
1437
  items[items.length - 1].content.push(sub.node);
1319
1438
  } else {
@@ -1324,7 +1443,7 @@ async function parseListBlock(lines, startIdx, baseIndent, instance) {
1324
1443
  }
1325
1444
  if (ordered === null) ordered = m.ordered; // list type set by first item
1326
1445
  else if (m.ordered !== ordered) break; // marker switched: caller starts a new list
1327
- const inlineContent = await parseInlineFormatting(m.text, instance);
1446
+ const inlineContent = await parseInlineFormatting(m.text, instance, issueKey);
1328
1447
  items.push({
1329
1448
  type: "listItem",
1330
1449
  content: [{ type: "paragraph", content: inlineContent }],
@@ -1338,7 +1457,7 @@ async function parseListBlock(lines, startIdx, baseIndent, instance) {
1338
1457
  }
1339
1458
 
1340
1459
  // Parse text with markdown formatting and @mentions, build ADF content
1341
- async function buildCommentADF(text, instance = defaultInstance) {
1460
+ async function buildCommentADF(text, instance = defaultInstance, issueKey = null) {
1342
1461
  // Sanitize: replace em dashes and en dashes with hyphen
1343
1462
  text = text.replace(/[—–]/g, "-");
1344
1463
 
@@ -1387,7 +1506,7 @@ async function buildCommentADF(text, instance = defaultInstance) {
1387
1506
  if (headingMatch) {
1388
1507
  const level = headingMatch[1].length;
1389
1508
  const headingText = headingMatch[2];
1390
- const inlineContent = await parseInlineFormatting(headingText, instance);
1509
+ const inlineContent = await parseInlineFormatting(headingText, instance, issueKey);
1391
1510
  content.push({
1392
1511
  type: "heading",
1393
1512
  attrs: { level },
@@ -1404,7 +1523,7 @@ async function buildCommentADF(text, instance = defaultInstance) {
1404
1523
  quoteLines.push(lines[i].trim().substring(2));
1405
1524
  i++;
1406
1525
  }
1407
- const quoteContent = await parseInlineFormatting(quoteLines.join("\n"), instance);
1526
+ const quoteContent = await parseInlineFormatting(quoteLines.join("\n"), instance, issueKey);
1408
1527
  content.push({
1409
1528
  type: "blockquote",
1410
1529
  content: [{ type: "paragraph", content: quoteContent }],
@@ -1436,7 +1555,7 @@ async function buildCommentADF(text, instance = defaultInstance) {
1436
1555
  const cellType = r === 0 && isHeader ? "tableHeader" : "tableCell";
1437
1556
  const adfCells = [];
1438
1557
  for (const cellText of tableRows[r]) {
1439
- const inlineContent = await parseInlineFormatting(cellText, instance);
1558
+ const inlineContent = await parseInlineFormatting(cellText, instance, issueKey);
1440
1559
  adfCells.push({
1441
1560
  type: cellType,
1442
1561
  content: [{ type: "paragraph", content: inlineContent }],
@@ -1457,6 +1576,7 @@ async function buildCommentADF(text, instance = defaultInstance) {
1457
1576
  i,
1458
1577
  listStart.indent,
1459
1578
  instance,
1579
+ issueKey,
1460
1580
  );
1461
1581
  content.push(node);
1462
1582
  i = nextIndex;
@@ -1478,7 +1598,7 @@ async function buildCommentADF(text, instance = defaultInstance) {
1478
1598
  !(lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|"))
1479
1599
  ) {
1480
1600
  if (paragraphContent.length > 0) paragraphContent.push({ type: "hardBreak" });
1481
- const inlineNodes = await parseInlineFormatting(lines[i].trim(), instance);
1601
+ const inlineNodes = await parseInlineFormatting(lines[i].trim(), instance, issueKey);
1482
1602
  paragraphContent.push(...inlineNodes);
1483
1603
  i++;
1484
1604
  }
@@ -2340,7 +2460,7 @@ async function getChangelogsBulk(jql, maxResults = 50, instance = defaultInstanc
2340
2460
 
2341
2461
  const SERVER_INSTRUCTIONS = `# jira-mcp usage rules
2342
2462
 
2343
- - \`instance\` (optional, on most tools): multi-instance setups only. Omit it — it auto-detects from the issue key prefix, else uses the default instance.
2463
+ - \`instance\` (optional, on most tools): multi-instance setups only. Omit it — it auto-detects from the issue key prefix, else uses this session's default instance (see \`jira_list_instances\`).
2344
2464
  - \`dryRun\` (optional, on mutating tools): if true, returns the HTTP request that would be sent (method, endpoint, body) without calling Atlassian. Multi-step tools only show the first write.
2345
2465
  ${confluenceMode !== "always" ? "- Confluence tools are hidden until activated: call `confluence_enable` first when a task needs Confluence (pages, spaces, page comments, labels, attachments).\n" : ""}
2346
2466
  ## Writing Jira comments (jira_add_comment / jira_reply_comment / jira_edit_comment)
@@ -2352,7 +2472,7 @@ After the tool succeeds, its response echoes the posted comment as a markdown bl
2352
2472
  Structure with Markdown: a one/two-sentence overview, then ## / ### headings (Overview, Details, Acceptance Criteria), '-' bullet lists, numbered steps, tables for structured data, **bold** key terms, fenced \`\`\` blocks for code/payloads. Blank line between blocks. Concise and scannable. Never use em/en dashes.`;
2353
2473
 
2354
2474
  const server = new Server(
2355
- { name: "jira-mcp", version: "1.0.0" },
2475
+ { name: "jira-mcp", version: PKG_VERSION, icons: ICONS },
2356
2476
  { capabilities: { tools: { listChanged: true } }, instructions: SERVER_INSTRUCTIONS },
2357
2477
  );
2358
2478
 
@@ -2740,7 +2860,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2740
2860
  },
2741
2861
  setDefault: {
2742
2862
  type: "boolean",
2743
- description: "Set this instance as the default (default: false)",
2863
+ description: "Set this instance as the config default (default: false). JIRA_MCP_INSTANCE or an instance whose paths contain the working directory still takes precedence for the current session.",
2744
2864
  },
2745
2865
  defaultTeam: {
2746
2866
  type: "string",
@@ -2776,7 +2896,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2776
2896
  {
2777
2897
  name: "jira_list_instances",
2778
2898
  description:
2779
- "List all configured Jira instances with their names, URLs, project prefixes, and which is the default.",
2899
+ "List all configured Jira instances with their names, URLs, project prefixes, paths, and which is the session default and why (env, path, or config).",
2780
2900
  inputSchema: {
2781
2901
  type: "object",
2782
2902
  properties: {},
@@ -3858,6 +3978,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3858
3978
  {},
3859
3979
  inst,
3860
3980
  );
3981
+ } else if (!hasAssignmentContext && (!users || users.length === 0)) {
3982
+ users = await searchAssignableUsersInProjects(inst, args.query, maxResults);
3861
3983
  }
3862
3984
 
3863
3985
  if (!users || users.length === 0) {
@@ -3947,7 +4069,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3947
4069
  } else if (name === "jira_add_comment") {
3948
4070
  const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
3949
4071
  // Build ADF content with mention support
3950
- const adfContent = await buildCommentADF(args.comment, inst);
4072
+ const adfContent = await buildCommentADF(args.comment, inst, args.issueKey);
3951
4073
  const body = {
3952
4074
  body: {
3953
4075
  version: 1,
@@ -4042,7 +4164,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4042
4164
  } else if (name === "jira_edit_comment") {
4043
4165
  const inst = args.instance ? getInstanceByName(args.instance) : getInstanceForKey(args.issueKey);
4044
4166
  // Build ADF content with mention support
4045
- const adfContent = await buildCommentADF(args.comment, inst);
4167
+ const adfContent = await buildCommentADF(args.comment, inst, args.issueKey);
4046
4168
  const body = {
4047
4169
  body: {
4048
4170
  version: 1,
@@ -4455,10 +4577,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4455
4577
  instances.push(savedInstance);
4456
4578
  activeInstance = savedInstance;
4457
4579
  }
4458
- defaultInstance =
4459
- instances.find((instance) => instance.name === persisted.defaultInstance) ||
4460
- instances[0];
4461
4580
  rawConfig.defaultInstance = persisted.defaultInstance;
4581
+ applySessionDefault();
4462
4582
 
4463
4583
  const action = persisted.isUpdate ? "Updated" : "Added";
4464
4584
  const savedBaseUrl = activeInstance.baseUrl;
@@ -4466,7 +4586,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4466
4586
  const projectTeams = activeInstance.projectTeams || {};
4467
4587
  let text = `${action} instance "${instName}" (${savedBaseUrl}).`;
4468
4588
  if (savedProjects.length > 0) text += ` Projects: ${savedProjects.join(", ")}.`;
4469
- if (args.setDefault) text += " Set as default.";
4589
+ if (args.setDefault) {
4590
+ text += " Set as default.";
4591
+ if (defaultInstance.name !== instName) {
4592
+ text += ` This session keeps using "${defaultInstance.name}" (from ${describeSessionDefault()}).`;
4593
+ }
4594
+ }
4470
4595
  if (projectTeamChange) {
4471
4596
  const pt = projectTeams[projectTeamChange.key];
4472
4597
  if (pt === "none") {
@@ -4528,10 +4653,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4528
4653
  // Update in-memory state only after the atomic config replacement succeeds.
4529
4654
  const idx = instances.findIndex((i) => i.name === instName);
4530
4655
  if (idx >= 0) instances.splice(idx, 1);
4531
- defaultInstance =
4532
- instances.find((instance) => instance.name === removal.defaultInstance) ||
4533
- instances[0];
4534
4656
  rawConfig.defaultInstance = removal.defaultInstance;
4657
+ applySessionDefault();
4535
4658
 
4536
4659
  return { content: [{ type: "text", text: `Removed instance "${instName}".` }] };
4537
4660
 
@@ -4539,12 +4662,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4539
4662
  if (instances.length === 0) {
4540
4663
  return { content: [{ type: "text", text: "No instances configured." }] };
4541
4664
  }
4542
- const currentDefault = rawConfig.defaultInstance || instances[0].name;
4665
+ const configDefault =
4666
+ instances.find((inst) => inst.name === rawConfig.defaultInstance) || instances[0];
4543
4667
  let text = `# Configured Jira Instances (${instances.length})\n\n`;
4668
+ text += `Session default: **${defaultInstance.name}** (from ${describeSessionDefault()}). Config default: ${configDefault.name}.\n`;
4669
+ if (sessionDefault.ignoredEnv) {
4670
+ text += `JIRA_MCP_INSTANCE "${sessionDefault.ignoredEnv}" does not match a configured instance and is ignored.\n`;
4671
+ }
4672
+ text += "\n";
4544
4673
  const missingTeam = [];
4545
4674
  for (const inst of instances) {
4546
- const isDefault = inst.name === currentDefault ? " **(default)**" : "";
4675
+ let isDefault = inst.name === defaultInstance.name ? " **(default)**" : "";
4676
+ if (inst.name === configDefault.name && inst.name !== defaultInstance.name) {
4677
+ isDefault = " (config default)";
4678
+ }
4547
4679
  const projs = inst.projects?.length > 0 ? `\n Projects: ${inst.projects.join(", ")}` : "";
4680
+ const paths = Array.isArray(inst.paths) && inst.paths.length > 0 ? `\n Paths: ${inst.paths.join(", ")}` : "";
4548
4681
  const team = inst.defaultTeam === "none" ? "\n Default team: None (disabled)" : inst.defaultTeam ? `\n Default team: ${inst.defaultTeam.name}` : "";
4549
4682
  let projTeams = "";
4550
4683
  if (inst.projectTeams && Object.keys(inst.projectTeams).length > 0) {
@@ -4553,7 +4686,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4553
4686
  projTeams += `\n ${pk} team: ${ptName}`;
4554
4687
  }
4555
4688
  }
4556
- text += `- **${inst.name}**${isDefault}: ${inst.baseUrl} (${inst.email})${projs}${team}${projTeams}\n`;
4689
+ text += `- **${inst.name}**${isDefault}: ${inst.baseUrl} (${inst.email})${projs}${paths}${team}${projTeams}\n`;
4557
4690
  if (!inst.defaultTeam) missingTeam.push(inst);
4558
4691
  }
4559
4692
  if (missingTeam.length > 0) {
@@ -5768,6 +5901,7 @@ if (typeof module !== "undefined") {
5768
5901
  checkRateLimit, _setClockForTests, _resetBucketsForTests,
5769
5902
  extractRoutingPrefixes, probeJiraProject, discoverProjectInstance,
5770
5903
  persistDiscoveredProjects, resolveInstanceForTool, _injectDryRunSchema,
5904
+ resolveSessionDefault,
5771
5905
  AUDIT_LOG_PATH,
5772
5906
  };
5773
5907
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rui.branco/jira-mcp",
3
- "version": "1.7.12",
3
+ "version": "1.7.14",
4
4
  "description": "Jira & Confluence MCP server for any MCP-compatible AI client (Claude Code, Codex, Antigravity, Cursor, etc.) - fetch tickets, search with JQL, update tickets, manage comments, change status, get Figma designs. Optional per-tool scopes, dry-run, audit log, and rate limiting.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -9,7 +9,9 @@
9
9
  "files": [
10
10
  "index.js",
11
11
  "setup.js",
12
- "config-store.js"
12
+ "config-store.js",
13
+ "icons.js",
14
+ "assets/icon.svg"
13
15
  ],
14
16
  "scripts": {
15
17
  "setup": "node setup.js",