@rui.branco/jira-mcp 1.7.6 → 1.7.7

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/README.md +13 -0
  2. package/index.js +189 -99
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -225,6 +225,19 @@ For Server / Data Center deployments where Confluence lives on a different host,
225
225
 
226
226
  **Why ADF for `confluence_get_page`?** The v1 `body.atlas_doc_format` expand is unreliable on modern Cloud pages — table cell background colors and some newer node types only survive the v2 `/api/v2/pages/<id>?body-format=atlas_doc_format` endpoint. `confluence_get_page` therefore does both requests in parallel and merges the v2 ADF body onto the v1 response, so callers get `body.view`, `body.storage` and `body.atlas_doc_format` in one shot.
227
227
 
228
+ ### Confluence On-Demand Activation
229
+
230
+ By default, the 21 `confluence_*` tools are hidden from `tools/list` to reduce token footprint. A lightweight `confluence_enable` gateway tool is exposed instead. When a task needs Confluence, the client calls `confluence_enable` and the full tool set appears (the server sends `notifications/tools/list_changed`). Activation is per-process (not persisted to config) and does not bypass instance scopes.
231
+
232
+ To expose Confluence tools from boot (skip the activation step), set the `confluence` config key or the `JIRA_MCP_CONFLUENCE` environment variable:
233
+
234
+ | Method | Value |
235
+ |--------|-------|
236
+ | Config (`~/.config/jira-mcp/config.json`) | `"confluence": "always"` |
237
+ | Environment variable | `JIRA_MCP_CONFLUENCE=always` |
238
+
239
+ The default value is `"on-demand"`.
240
+
228
241
  ### Configuration
229
242
 
230
243
  Config stored at `~/.config/jira-mcp/config.json`:
package/index.js CHANGED
@@ -53,6 +53,15 @@ const jiraConfigPath = path.join(
53
53
  );
54
54
  const rawConfig = JSON.parse(fs.readFileSync(jiraConfigPath, "utf8"));
55
55
 
56
+ // Confluence tool visibility: 'on-demand' (default) hides the confluence_*
57
+ // tools until confluence_enable is called; 'always' exposes them from boot.
58
+ // Env JIRA_MCP_CONFLUENCE overrides the config key.
59
+ const confluenceMode = String(process.env.JIRA_MCP_CONFLUENCE || rawConfig.confluence || "on-demand").toLowerCase();
60
+ if (confluenceMode !== "always" && confluenceMode !== "on-demand") {
61
+ console.error(`jira-mcp: invalid confluence mode "${confluenceMode}", falling back to "on-demand"`);
62
+ }
63
+ let confluenceActive = confluenceMode === "always";
64
+
56
65
  // Normalize config to multi-instance format
57
66
  let instances;
58
67
  if (rawConfig.instances) {
@@ -181,6 +190,8 @@ const TOOL_METADATA = {
181
190
  jira_delete_ticket: { product: "jira", op: "destructive", category: "ticket" },
182
191
  jira_remove_attachment: { product: "jira", op: "destructive", category: "attachment" },
183
192
  jira_remove_instance: { product: "jira", op: "destructive", category: "config" },
193
+ // ---- Confluence activation ----
194
+ confluence_enable: { product: "confluence", op: "read" },
184
195
  // ---- Confluence reads ----
185
196
  confluence_get_spaces: { product: "confluence", op: "read" },
186
197
  confluence_get_space: { product: "confluence", op: "read" },
@@ -758,7 +769,10 @@ function extractText(content, urls = []) {
758
769
  } else if (node.type === "hardBreak") {
759
770
  text += "\n";
760
771
  } else if (node.type === "mention") {
761
- text += `@${node.attrs?.text || "user"}`;
772
+ // attrs.text already includes the leading "@" (e.g. "@Rui Branco").
773
+ // Don't prepend a second one.
774
+ const mentionText = node.attrs?.text || "@user";
775
+ text += mentionText.startsWith("@") ? mentionText : `@${mentionText}`;
762
776
  } else if (node.type === "mediaGroup" || node.type === "mediaSingle") {
763
777
  text += "[image attachment]\n";
764
778
  } else if (
@@ -965,6 +979,54 @@ function autoLinkTextNodes(nodes, instance = defaultInstance) {
965
979
  return result;
966
980
  }
967
981
 
982
+ // Detect a markdown list item line. Returns {indent, ordered, text} or null.
983
+ // Bullets: -, *, + followed by a space. Ordered: "1." or "1)" followed by a space.
984
+ // The marker must be followed by whitespace so inline emphasis (*italic*) and
985
+ // horizontal rules (***) are NOT misread as list items.
986
+ function listItemMatch(line) {
987
+ const m = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
988
+ if (!m) return null;
989
+ return { indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] };
990
+ }
991
+
992
+ // Recursively parse a (possibly nested) markdown list starting at lines[startIdx].
993
+ // baseIndent is the indentation of items at this level; more-indented items are
994
+ // folded into the previous item as a nested list. Returns {node, nextIndex}.
995
+ async function parseListBlock(lines, startIdx, baseIndent, instance) {
996
+ const items = [];
997
+ let i = startIdx;
998
+ let ordered = null;
999
+ while (i < lines.length) {
1000
+ if (!lines[i].trim()) break; // blank line ends the list
1001
+ const m = listItemMatch(lines[i]);
1002
+ if (!m) break;
1003
+ if (m.indent < baseIndent) break; // dedent: belongs to a parent list
1004
+ if (m.indent > baseIndent) {
1005
+ // Deeper indent: nest under the previous item at this level.
1006
+ const sub = await parseListBlock(lines, i, m.indent, instance);
1007
+ if (items.length > 0) {
1008
+ items[items.length - 1].content.push(sub.node);
1009
+ } else {
1010
+ items.push({ type: "listItem", content: [sub.node] });
1011
+ }
1012
+ i = sub.nextIndex;
1013
+ continue;
1014
+ }
1015
+ if (ordered === null) ordered = m.ordered; // list type set by first item
1016
+ else if (m.ordered !== ordered) break; // marker switched: caller starts a new list
1017
+ const inlineContent = await parseInlineFormatting(m.text, instance);
1018
+ items.push({
1019
+ type: "listItem",
1020
+ content: [{ type: "paragraph", content: inlineContent }],
1021
+ });
1022
+ i++;
1023
+ }
1024
+ return {
1025
+ node: { type: ordered ? "orderedList" : "bulletList", content: items },
1026
+ nextIndex: i,
1027
+ };
1028
+ }
1029
+
968
1030
  // Parse text with markdown formatting and @mentions, build ADF content
969
1031
  async function buildCommentADF(text, instance = defaultInstance) {
970
1032
  // Sanitize: replace em dashes and en dashes with hyphen
@@ -1077,35 +1139,17 @@ async function buildCommentADF(text, instance = defaultInstance) {
1077
1139
  continue;
1078
1140
  }
1079
1141
 
1080
- // --- Bullet list (- items) ---
1081
- if (trimmed.startsWith("- ")) {
1082
- const listItems = [];
1083
- while (i < lines.length && lines[i].trim().startsWith("- ")) {
1084
- const itemText = lines[i].trim().substring(2);
1085
- const inlineContent = await parseInlineFormatting(itemText, instance);
1086
- listItems.push({
1087
- type: "listItem",
1088
- content: [{ type: "paragraph", content: inlineContent }],
1089
- });
1090
- i++;
1091
- }
1092
- content.push({ type: "bulletList", content: listItems });
1093
- continue;
1094
- }
1095
-
1096
- // --- Ordered list (1. items) ---
1097
- if (/^\d+\.\s/.test(trimmed)) {
1098
- const listItems = [];
1099
- while (i < lines.length && /^\d+\.\s/.test(lines[i].trim())) {
1100
- const itemText = lines[i].trim().replace(/^\d+\.\s/, "");
1101
- const inlineContent = await parseInlineFormatting(itemText, instance);
1102
- listItems.push({
1103
- type: "listItem",
1104
- content: [{ type: "paragraph", content: inlineContent }],
1105
- });
1106
- i++;
1107
- }
1108
- content.push({ type: "orderedList", content: listItems });
1142
+ // --- Lists (bullet -,*,+ or ordered 1./1)) with nesting ---
1143
+ const listStart = listItemMatch(line);
1144
+ if (listStart) {
1145
+ const { node, nextIndex } = await parseListBlock(
1146
+ lines,
1147
+ i,
1148
+ listStart.indent,
1149
+ instance,
1150
+ );
1151
+ content.push(node);
1152
+ i = nextIndex;
1109
1153
  continue;
1110
1154
  }
1111
1155
 
@@ -1115,13 +1159,12 @@ async function buildCommentADF(text, instance = defaultInstance) {
1115
1159
  i < lines.length &&
1116
1160
  lines[i].trim() &&
1117
1161
  !lines[i].trim().startsWith("```") &&
1118
- !lines[i].trim().startsWith("# ") &&
1119
- !lines[i].trim().startsWith("## ") &&
1120
- !lines[i].trim().startsWith("### ") &&
1162
+ !/^#{1,6}\s/.test(lines[i].trim()) &&
1121
1163
  !lines[i].trim().startsWith("> ") &&
1122
- !lines[i].trim().startsWith("- ") &&
1123
- !/^\d+\.\s/.test(lines[i].trim()) &&
1164
+ !listItemMatch(lines[i]) &&
1124
1165
  !/^-{3,}$/.test(lines[i].trim()) &&
1166
+ !/^\*{3,}$/.test(lines[i].trim()) &&
1167
+ !/^_{3,}$/.test(lines[i].trim()) &&
1125
1168
  !(lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|"))
1126
1169
  ) {
1127
1170
  if (paragraphContent.length > 0) paragraphContent.push({ type: "hardBreak" });
@@ -1985,9 +2028,20 @@ async function getChangelogsBulk(jql, maxResults = 50, instance = defaultInstanc
1985
2028
 
1986
2029
  // ============ MCP SERVER ============
1987
2030
 
2031
+ const SERVER_INSTRUCTIONS = `# jira-mcp usage rules
2032
+
2033
+ - \`instance\` (optional, on most tools): multi-instance setups only. Omit it — it auto-detects from the issue key prefix, else uses the default instance.
2034
+ - \`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.
2035
+ ${confluenceMode !== "always" ? "- Confluence tools are hidden until activated: call `confluence_enable` first when a task needs Confluence (pages, spaces, page comments, labels, attachments).\n" : ""}
2036
+ ## Writing Jira comments (jira_add_comment / jira_reply_comment / jira_edit_comment)
2037
+ Write one short conversational paragraph: start with @DisplayName of who to notify (NOT [~accountId:...] syntax), say what changed, and end with the next step (e.g. 'Ready for verification.'). Keep it HIGH LEVEL for stakeholders/PO/QA — WHAT changed and the impact, not how it was built. Include technical detail (endpoints, payloads, API contracts) ONLY when the reader is a developer and it makes sense; otherwise leave out code, file paths, branch names, commit hashes, and framework-internal terms. NEVER use em dashes (—) or en dashes (–) — use commas, periods, or rewrite. Avoid bullet lists, headings, and bold unless the update genuinely needs them.
2038
+
2039
+ ## Writing descriptions (jira_create_ticket / jira_update_ticket / jira_create_subtask)
2040
+ 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.`;
2041
+
1988
2042
  const server = new Server(
1989
2043
  { name: "jira-mcp", version: "1.0.0" },
1990
- { capabilities: { tools: {} } },
2044
+ { capabilities: { tools: { listChanged: true } }, instructions: SERVER_INSTRUCTIONS },
1991
2045
  );
1992
2046
 
1993
2047
  // Inject `dryRun` into the schema of any mutating tool so MCP clients with
@@ -2006,13 +2060,16 @@ function _injectDryRunSchema(tool) {
2006
2060
  ...props,
2007
2061
  dryRun: {
2008
2062
  type: "boolean",
2009
- description: "If true, return the HTTP request that would be sent (method, endpoint, body) without calling Atlassian. Multi-step tools only show the first write.",
2063
+ description: "Preview: return the would-be HTTP request without executing.",
2010
2064
  },
2011
2065
  },
2012
2066
  },
2013
2067
  };
2014
2068
  }
2015
2069
 
2070
+ // Additive formatting guidance moved to SERVER_INSTRUCTIONS to reduce per-tool
2071
+ // token cost. Field descriptions retain their short base text only.
2072
+
2016
2073
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2017
2074
  const rawTools = [
2018
2075
  {
@@ -2024,7 +2081,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2024
2081
  properties: {
2025
2082
  instance: {
2026
2083
  type: "string",
2027
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2084
+ description: "Instance name (optional).",
2028
2085
  },
2029
2086
  },
2030
2087
  required: [],
@@ -2052,7 +2109,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2052
2109
  },
2053
2110
  instance: {
2054
2111
  type: "string",
2055
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2112
+ description: "Instance name (optional).",
2056
2113
  },
2057
2114
  },
2058
2115
  required: ["issueKey"],
@@ -2078,7 +2135,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2078
2135
  },
2079
2136
  instance: {
2080
2137
  type: "string",
2081
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2138
+ description: "Instance name (optional).",
2082
2139
  },
2083
2140
  },
2084
2141
  required: ["jql"],
@@ -2087,7 +2144,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2087
2144
  {
2088
2145
  name: "jira_add_comment",
2089
2146
  description:
2090
- "Add a comment to a Jira ticket. IMPORTANT RULES: (1) Write comments at a HIGH LEVEL for stakeholders, product owners, and managers. Focus on WHAT was done and the business impact, NOT how it was done. Example: 'Implemented the new filtering feature for the dashboard' instead of 'Added a useEffect hook with debounced API calls to filter endpoint'. (2) NEVER mention technical details: no code, no function names, no file paths, no git branches, no commit hashes, no 'pushed to main', no framework-specific terms (React, Angular, hooks, components, etc.). (3) Use @DisplayName (e.g. @Julia Pereszta) for mentions, NOT [~accountId:...] syntax. (4) NEVER use em dashes (—) or en dashes (–), use commas, periods, or rewrite the sentence instead.",
2147
+ "Add a comment to a Jira ticket. Follow the server instructions' comment style rules.",
2091
2148
  inputSchema: {
2092
2149
  type: "object",
2093
2150
  properties: {
@@ -2095,10 +2152,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2095
2152
  type: "string",
2096
2153
  description: "The Jira issue key (e.g., MODS-123)",
2097
2154
  },
2098
- comment: { type: "string", description: "The comment text to add" },
2155
+ comment: {
2156
+ type: "string",
2157
+ description: "The comment text to add.",
2158
+ },
2099
2159
  instance: {
2100
2160
  type: "string",
2101
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2161
+ description: "Instance name (optional).",
2102
2162
  },
2103
2163
  },
2104
2164
  required: ["issueKey", "comment"],
@@ -2120,10 +2180,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2120
2180
  description:
2121
2181
  "The ID of the comment to reply to. Use jira_get_ticket to see comments and their IDs.",
2122
2182
  },
2123
- reply: { type: "string", description: "The reply text" },
2183
+ reply: {
2184
+ type: "string",
2185
+ description: "The reply text.",
2186
+ },
2124
2187
  instance: {
2125
2188
  type: "string",
2126
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2189
+ description: "Instance name (optional).",
2127
2190
  },
2128
2191
  },
2129
2192
  required: ["issueKey", "commentId", "reply"],
@@ -2132,7 +2195,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2132
2195
  {
2133
2196
  name: "jira_edit_comment",
2134
2197
  description:
2135
- "Edit an existing comment on a Jira ticket. Replaces the comment text. IMPORTANT: Use @DisplayName (e.g. @Julia Pereszta) for mentions — NOT [~accountId:...] syntax. Keep comments non-technical and user-facing. Never mention git details like 'pushed to main', branch names, or technical implementation details.",
2198
+ "Edit an existing comment on a Jira ticket (replaces the text). Follow the server instructions' comment style rules.",
2136
2199
  inputSchema: {
2137
2200
  type: "object",
2138
2201
  properties: {
@@ -2145,10 +2208,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2145
2208
  description:
2146
2209
  "The ID of the comment to edit. Use jira_get_ticket to see comments and their IDs.",
2147
2210
  },
2148
- comment: { type: "string", description: "The new comment text" },
2211
+ comment: {
2212
+ type: "string",
2213
+ description: "The new comment text.",
2214
+ },
2149
2215
  instance: {
2150
2216
  type: "string",
2151
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2217
+ description: "Instance name (optional).",
2152
2218
  },
2153
2219
  },
2154
2220
  required: ["issueKey", "commentId", "comment"],
@@ -2172,7 +2238,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2172
2238
  },
2173
2239
  instance: {
2174
2240
  type: "string",
2175
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2241
+ description: "Instance name (optional).",
2176
2242
  },
2177
2243
  },
2178
2244
  required: ["issueKey", "commentId"],
@@ -2201,7 +2267,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2201
2267
  },
2202
2268
  instance: {
2203
2269
  type: "string",
2204
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2270
+ description: "Instance name (optional).",
2205
2271
  },
2206
2272
  },
2207
2273
  required: ["issueKey"],
@@ -2262,7 +2328,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2262
2328
  },
2263
2329
  instance: {
2264
2330
  type: "string",
2265
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2331
+ description: "Instance name (optional).",
2266
2332
  },
2267
2333
  },
2268
2334
  required: ["issueKey"],
@@ -2286,7 +2352,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2286
2352
  },
2287
2353
  instance: {
2288
2354
  type: "string",
2289
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2355
+ description: "Instance name (optional).",
2290
2356
  },
2291
2357
  },
2292
2358
  required: ["query"],
@@ -2316,7 +2382,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2316
2382
  },
2317
2383
  instance: {
2318
2384
  type: "string",
2319
- description: "Instance name (for multi-instance setups with jql). Auto-detected from issueKey if provided.",
2385
+ description: "Instance name (optional).",
2320
2386
  },
2321
2387
  },
2322
2388
  required: [],
@@ -2408,7 +2474,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2408
2474
  },
2409
2475
  instance: {
2410
2476
  type: "string",
2411
- description: "Instance name. Uses default instance if omitted.",
2477
+ description: "Instance name (optional).",
2412
2478
  },
2413
2479
  },
2414
2480
  required: [],
@@ -2435,7 +2501,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2435
2501
  },
2436
2502
  description: {
2437
2503
  type: "string",
2438
- description: "The issue description text. Supports @mentions via @DisplayName.",
2504
+ description:
2505
+ "The issue description text. Supports @mentions via @DisplayName.",
2439
2506
  },
2440
2507
  assignee: {
2441
2508
  type: "string",
@@ -2478,7 +2545,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2478
2545
  },
2479
2546
  instance: {
2480
2547
  type: "string",
2481
- description: "Instance name override. Auto-detected from project key if omitted.",
2548
+ description: "Instance name (optional).",
2482
2549
  },
2483
2550
  },
2484
2551
  required: ["projectKey", "issueType", "summary"],
@@ -2501,7 +2568,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2501
2568
  },
2502
2569
  description: {
2503
2570
  type: "string",
2504
- description: "The subtask description text. Supports @mentions via @DisplayName.",
2571
+ description:
2572
+ "The subtask description text. Supports @mentions via @DisplayName.",
2505
2573
  },
2506
2574
  assignee: {
2507
2575
  type: "string",
@@ -2522,7 +2590,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2522
2590
  },
2523
2591
  instance: {
2524
2592
  type: "string",
2525
- description: "Instance name override. Auto-detected from parent issue key prefix if omitted.",
2593
+ description: "Instance name (optional).",
2526
2594
  },
2527
2595
  },
2528
2596
  required: ["parentKey", "summary"],
@@ -2553,7 +2621,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2553
2621
  },
2554
2622
  instance: {
2555
2623
  type: "string",
2556
- description: "Instance name override. Auto-detected from inward issue key prefix if omitted.",
2624
+ description: "Instance name (optional).",
2557
2625
  },
2558
2626
  },
2559
2627
  required: ["inwardIssueKey", "outwardIssueKey", "linkType"],
@@ -2576,7 +2644,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2576
2644
  },
2577
2645
  instance: {
2578
2646
  type: "string",
2579
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2647
+ description: "Instance name (optional).",
2580
2648
  },
2581
2649
  },
2582
2650
  required: ["issueKey"],
@@ -2607,7 +2675,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2607
2675
  },
2608
2676
  instance: {
2609
2677
  type: "string",
2610
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2678
+ description: "Instance name (optional).",
2611
2679
  },
2612
2680
  },
2613
2681
  required: ["issueKey"],
@@ -2622,7 +2690,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2622
2690
  properties: {
2623
2691
  instance: {
2624
2692
  type: "string",
2625
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2693
+ description: "Instance name (optional).",
2626
2694
  },
2627
2695
  },
2628
2696
  required: [],
@@ -2641,7 +2709,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2641
2709
  },
2642
2710
  instance: {
2643
2711
  type: "string",
2644
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2712
+ description: "Instance name (optional).",
2645
2713
  },
2646
2714
  },
2647
2715
  required: ["issueKey"],
@@ -2664,7 +2732,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2664
2732
  },
2665
2733
  instance: {
2666
2734
  type: "string",
2667
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2735
+ description: "Instance name (optional).",
2668
2736
  },
2669
2737
  },
2670
2738
  required: ["issueKey", "linkedIssueKey"],
@@ -2691,7 +2759,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2691
2759
  },
2692
2760
  instance: {
2693
2761
  type: "string",
2694
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2762
+ description: "Instance name (optional).",
2695
2763
  },
2696
2764
  },
2697
2765
  required: [],
@@ -2714,7 +2782,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2714
2782
  },
2715
2783
  instance: {
2716
2784
  type: "string",
2717
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2785
+ description: "Instance name (optional).",
2718
2786
  },
2719
2787
  },
2720
2788
  required: ["issueKey", "accountId"],
@@ -2737,7 +2805,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2737
2805
  },
2738
2806
  instance: {
2739
2807
  type: "string",
2740
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2808
+ description: "Instance name (optional).",
2741
2809
  },
2742
2810
  },
2743
2811
  required: ["issueKey", "accountId"],
@@ -2768,7 +2836,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2768
2836
  },
2769
2837
  instance: {
2770
2838
  type: "string",
2771
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2839
+ description: "Instance name (optional).",
2772
2840
  },
2773
2841
  },
2774
2842
  required: ["issueKey", "timeSpent"],
@@ -2787,7 +2855,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2787
2855
  },
2788
2856
  instance: {
2789
2857
  type: "string",
2790
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2858
+ description: "Instance name (optional).",
2791
2859
  },
2792
2860
  },
2793
2861
  required: ["issueKey"],
@@ -2818,7 +2886,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2818
2886
  },
2819
2887
  instance: {
2820
2888
  type: "string",
2821
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2889
+ description: "Instance name (optional).",
2822
2890
  },
2823
2891
  },
2824
2892
  required: ["issueKey"],
@@ -2842,7 +2910,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2842
2910
  },
2843
2911
  instance: {
2844
2912
  type: "string",
2845
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2913
+ description: "Instance name (optional).",
2846
2914
  },
2847
2915
  },
2848
2916
  required: ["sprintId", "issueKeys"],
@@ -2865,7 +2933,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2865
2933
  },
2866
2934
  instance: {
2867
2935
  type: "string",
2868
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2936
+ description: "Instance name (optional).",
2869
2937
  },
2870
2938
  },
2871
2939
  required: ["boardId"],
@@ -2892,7 +2960,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2892
2960
  },
2893
2961
  instance: {
2894
2962
  type: "string",
2895
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2963
+ description: "Instance name (optional).",
2896
2964
  },
2897
2965
  },
2898
2966
  required: [],
@@ -2911,7 +2979,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2911
2979
  },
2912
2980
  instance: {
2913
2981
  type: "string",
2914
- description: "Instance name override. Auto-detected from project key if omitted.",
2982
+ description: "Instance name (optional).",
2915
2983
  },
2916
2984
  },
2917
2985
  required: ["projectKey"],
@@ -2926,7 +2994,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2926
2994
  properties: {
2927
2995
  instance: {
2928
2996
  type: "string",
2929
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2997
+ description: "Instance name (optional).",
2930
2998
  },
2931
2999
  },
2932
3000
  required: [],
@@ -2945,7 +3013,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2945
3013
  },
2946
3014
  instance: {
2947
3015
  type: "string",
2948
- description: "Instance name override. Auto-detected from project key if omitted.",
3016
+ description: "Instance name (optional).",
2949
3017
  },
2950
3018
  },
2951
3019
  required: ["projectKey"],
@@ -2964,7 +3032,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2964
3032
  },
2965
3033
  instance: {
2966
3034
  type: "string",
2967
- description: "Instance name override. Auto-detected from project key if omitted.",
3035
+ description: "Instance name (optional).",
2968
3036
  },
2969
3037
  },
2970
3038
  required: ["projectKey"],
@@ -2979,7 +3047,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2979
3047
  properties: {
2980
3048
  instance: {
2981
3049
  type: "string",
2982
- description: "Instance name. Uses default instance if omitted.",
3050
+ description: "Instance name (optional).",
2983
3051
  },
2984
3052
  },
2985
3053
  required: [],
@@ -2993,7 +3061,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2993
3061
  type: "object",
2994
3062
  properties: {
2995
3063
  spaceKey: { type: "string", description: "Space key (e.g., 'ENG')." },
2996
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3064
+ instance: { type: "string", description: "Instance name (optional)." },
2997
3065
  },
2998
3066
  required: ["spaceKey"],
2999
3067
  },
@@ -3008,7 +3076,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3008
3076
  spaceKey: { type: "string", description: "Unique space key (letters/numbers). Will be uppercased." },
3009
3077
  name: { type: "string", description: "Human-readable space name." },
3010
3078
  description: { type: "string", description: "Optional plain-text description." },
3011
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3079
+ instance: { type: "string", description: "Instance name (optional)." },
3012
3080
  },
3013
3081
  required: ["spaceKey", "name"],
3014
3082
  },
@@ -3023,7 +3091,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3023
3091
  spaceKey: { type: "string", description: "Space key to update." },
3024
3092
  name: { type: "string", description: "New space name." },
3025
3093
  description: { type: "string", description: "New plain-text description." },
3026
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3094
+ instance: { type: "string", description: "Instance name (optional)." },
3027
3095
  },
3028
3096
  required: ["spaceKey"],
3029
3097
  },
@@ -3036,7 +3104,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3036
3104
  type: "object",
3037
3105
  properties: {
3038
3106
  spaceKey: { type: "string", description: "Space key to delete." },
3039
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3107
+ instance: { type: "string", description: "Instance name (optional)." },
3040
3108
  },
3041
3109
  required: ["spaceKey"],
3042
3110
  },
@@ -3052,7 +3120,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3052
3120
  spaceKey: { type: "string", description: "Optional space key to scope the search." },
3053
3121
  type: { type: "string", description: "Content type (default: page). e.g. page, blogpost, comment, attachment." },
3054
3122
  limit: { type: "number", description: "Max results (default: 25)." },
3055
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3123
+ instance: { type: "string", description: "Instance name (optional)." },
3056
3124
  },
3057
3125
  required: ["query"],
3058
3126
  },
@@ -3067,7 +3135,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3067
3135
  spaceKey: { type: "string", description: "Optional space key to scope the query." },
3068
3136
  sinceDays: { type: "number", description: "Days back from now (default: 30)." },
3069
3137
  limit: { type: "number", description: "Max results (default: 25)." },
3070
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3138
+ instance: { type: "string", description: "Instance name (optional)." },
3071
3139
  },
3072
3140
  required: [],
3073
3141
  },
@@ -3081,7 +3149,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3081
3149
  properties: {
3082
3150
  spaceKey: { type: "string", description: "Space key (required)." },
3083
3151
  limit: { type: "number", description: "Max results (default: 50)." },
3084
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3152
+ instance: { type: "string", description: "Instance name (optional)." },
3085
3153
  },
3086
3154
  required: ["spaceKey"],
3087
3155
  },
@@ -3095,7 +3163,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3095
3163
  properties: {
3096
3164
  pageId: { type: "string", description: "Parent page ID (required)." },
3097
3165
  limit: { type: "number", description: "Max results (default: 50)." },
3098
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3166
+ instance: { type: "string", description: "Instance name (optional)." },
3099
3167
  },
3100
3168
  required: ["pageId"],
3101
3169
  },
@@ -3109,7 +3177,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3109
3177
  properties: {
3110
3178
  pageId: { type: "string", description: "Page ID (required)." },
3111
3179
  includeAdf: { type: "boolean", description: "Fetch ADF body via v2 API and merge (default: true)." },
3112
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3180
+ instance: { type: "string", description: "Instance name (optional)." },
3113
3181
  },
3114
3182
  required: ["pageId"],
3115
3183
  },
@@ -3123,7 +3191,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3123
3191
  properties: {
3124
3192
  pageId: { type: "string", description: "Page ID (required)." },
3125
3193
  limit: { type: "number", description: "Max results (default: 50)." },
3126
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3194
+ instance: { type: "string", description: "Instance name (optional)." },
3127
3195
  },
3128
3196
  required: ["pageId"],
3129
3197
  },
@@ -3138,7 +3206,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3138
3206
  pageId: { type: "string", description: "Page ID (required)." },
3139
3207
  body: { type: "string", description: "Comment body (plain text by default, or storage XHTML if format=storage)." },
3140
3208
  format: { type: "string", description: "'text' (default) or 'storage'." },
3141
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3209
+ instance: { type: "string", description: "Instance name (optional)." },
3142
3210
  },
3143
3211
  required: ["pageId", "body"],
3144
3212
  },
@@ -3154,7 +3222,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3154
3222
  title: { type: "string", description: "New page title (optional — retains current if omitted)." },
3155
3223
  body: { type: "string", description: "New body as storage XHTML (optional — retains current if omitted)." },
3156
3224
  version: { type: "number", description: "Explicit new version number. If omitted, current+1 is computed." },
3157
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3225
+ instance: { type: "string", description: "Instance name (optional)." },
3158
3226
  },
3159
3227
  required: ["pageId"],
3160
3228
  },
@@ -3170,7 +3238,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3170
3238
  title: { type: "string", description: "Page title (required)." },
3171
3239
  body: { type: "string", description: "Storage-format XHTML body (required)." },
3172
3240
  parentId: { type: "string", description: "Optional parent page ID to nest under." },
3173
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3241
+ instance: { type: "string", description: "Instance name (optional)." },
3174
3242
  },
3175
3243
  required: ["spaceKey", "title", "body"],
3176
3244
  },
@@ -3182,7 +3250,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3182
3250
  type: "object",
3183
3251
  properties: {
3184
3252
  pageId: { type: "string", description: "Page ID (required)." },
3185
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3253
+ instance: { type: "string", description: "Instance name (optional)." },
3186
3254
  },
3187
3255
  required: ["pageId"],
3188
3256
  },
@@ -3194,7 +3262,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3194
3262
  type: "object",
3195
3263
  properties: {
3196
3264
  pageId: { type: "string", description: "Page ID (required)." },
3197
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3265
+ instance: { type: "string", description: "Instance name (optional)." },
3198
3266
  },
3199
3267
  required: ["pageId"],
3200
3268
  },
@@ -3207,7 +3275,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3207
3275
  properties: {
3208
3276
  pageId: { type: "string", description: "Page ID (required)." },
3209
3277
  label: { type: "string", description: "Label name (required)." },
3210
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3278
+ instance: { type: "string", description: "Instance name (optional)." },
3211
3279
  },
3212
3280
  required: ["pageId", "label"],
3213
3281
  },
@@ -3220,7 +3288,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3220
3288
  properties: {
3221
3289
  pageId: { type: "string", description: "Page ID (required)." },
3222
3290
  label: { type: "string", description: "Label name (required)." },
3223
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3291
+ instance: { type: "string", description: "Instance name (optional)." },
3224
3292
  },
3225
3293
  required: ["pageId", "label"],
3226
3294
  },
@@ -3233,7 +3301,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3233
3301
  properties: {
3234
3302
  pageId: { type: "string", description: "Page ID (required)." },
3235
3303
  limit: { type: "number", description: "Max results (default: 50)." },
3236
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3304
+ instance: { type: "string", description: "Instance name (optional)." },
3237
3305
  },
3238
3306
  required: ["pageId"],
3239
3307
  },
@@ -3250,7 +3318,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3250
3318
  fileContent: { type: "string", description: "Base64-encoded file content (requires fileName)." },
3251
3319
  fileName: { type: "string", description: "File name (required with fileContent; optional with filePath)." },
3252
3320
  comment: { type: "string", description: "Optional attachment comment." },
3253
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3321
+ instance: { type: "string", description: "Instance name (optional)." },
3254
3322
  },
3255
3323
  required: ["pageId"],
3256
3324
  },
@@ -3264,13 +3332,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3264
3332
  properties: {
3265
3333
  pageId: { type: "string", description: "Page ID (required)." },
3266
3334
  filename: { type: "string", description: "Attachment filename (required)." },
3267
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3335
+ instance: { type: "string", description: "Instance name (optional)." },
3268
3336
  },
3269
3337
  required: ["pageId", "filename"],
3270
3338
  },
3271
3339
  },
3272
3340
  ];
3273
- return { tools: rawTools.map(_injectDryRunSchema) };
3341
+ let tools = rawTools;
3342
+ if (!confluenceActive) {
3343
+ tools = rawTools.filter(t => !t.name.startsWith("confluence_"));
3344
+ tools.push({
3345
+ name: "confluence_enable",
3346
+ description: "Activate the Confluence tool set (pages, spaces, page comments, labels, attachments, search). Call this first when a task needs Confluence; the confluence_* tools then become available. Activation only changes tool visibility for this server process; per-instance scopes still apply to every Confluence operation.",
3347
+ inputSchema: { type: "object", properties: {}, required: [] },
3348
+ });
3349
+ }
3350
+ return { tools: tools.map(_injectDryRunSchema) };
3274
3351
  });
3275
3352
 
3276
3353
  async function runWithPolicy(name, args, handler) {
@@ -3388,6 +3465,19 @@ async function runWithPolicy(name, args, handler) {
3388
3465
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
3389
3466
  const { name, arguments: args } = request.params;
3390
3467
 
3468
+ if (name === "confluence_enable") {
3469
+ const wasActive = confluenceActive;
3470
+ confluenceActive = true;
3471
+ if (!wasActive) server.sendToolListChanged().catch((e) => console.error("jira-mcp: tools/list_changed notification failed:", e?.message || e));
3472
+ return { content: [{ type: "text", text: wasActive ? "Confluence tools were already active." : "Confluence tools activated — the confluence_* tools are now available." }] };
3473
+ }
3474
+ // A client with a cached tool list may call a confluence_* tool directly
3475
+ // while hidden — auto-activate instead of failing.
3476
+ if (!confluenceActive && TOOL_METADATA[name]?.product === "confluence" && name !== "confluence_enable") {
3477
+ confluenceActive = true;
3478
+ server.sendToolListChanged().catch((e) => console.error("jira-mcp: tools/list_changed notification failed:", e?.message || e));
3479
+ }
3480
+
3391
3481
  return runWithPolicy(name, args, async () => {
3392
3482
  if (name === "jira_get_myself") {
3393
3483
  const inst = getInstanceByName(args.instance);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rui.branco/jira-mcp",
3
- "version": "1.7.6",
3
+ "version": "1.7.7",
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": {