@rui.branco/jira-mcp 1.7.6 → 1.7.8

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 +203 -102
  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 (
@@ -788,6 +802,14 @@ function extractTextSimple(content) {
788
802
  return result.text;
789
803
  }
790
804
 
805
+ // Prefix each line with "> " so echoed comments render as a markdown blockquote
806
+ function quoteText(text) {
807
+ return text
808
+ .split("\n")
809
+ .map((line) => (line.trim() ? `> ${line}` : ">"))
810
+ .join("\n");
811
+ }
812
+
791
813
  // ============ USER SEARCH & MENTIONS ============
792
814
 
793
815
  // Cache for user lookups to avoid repeated API calls
@@ -965,6 +987,54 @@ function autoLinkTextNodes(nodes, instance = defaultInstance) {
965
987
  return result;
966
988
  }
967
989
 
990
+ // Detect a markdown list item line. Returns {indent, ordered, text} or null.
991
+ // Bullets: -, *, + followed by a space. Ordered: "1." or "1)" followed by a space.
992
+ // The marker must be followed by whitespace so inline emphasis (*italic*) and
993
+ // horizontal rules (***) are NOT misread as list items.
994
+ function listItemMatch(line) {
995
+ const m = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
996
+ if (!m) return null;
997
+ return { indent: m[1].length, ordered: /\d/.test(m[2]), text: m[3] };
998
+ }
999
+
1000
+ // Recursively parse a (possibly nested) markdown list starting at lines[startIdx].
1001
+ // baseIndent is the indentation of items at this level; more-indented items are
1002
+ // folded into the previous item as a nested list. Returns {node, nextIndex}.
1003
+ async function parseListBlock(lines, startIdx, baseIndent, instance) {
1004
+ const items = [];
1005
+ let i = startIdx;
1006
+ let ordered = null;
1007
+ while (i < lines.length) {
1008
+ if (!lines[i].trim()) break; // blank line ends the list
1009
+ const m = listItemMatch(lines[i]);
1010
+ if (!m) break;
1011
+ if (m.indent < baseIndent) break; // dedent: belongs to a parent list
1012
+ if (m.indent > baseIndent) {
1013
+ // Deeper indent: nest under the previous item at this level.
1014
+ const sub = await parseListBlock(lines, i, m.indent, instance);
1015
+ if (items.length > 0) {
1016
+ items[items.length - 1].content.push(sub.node);
1017
+ } else {
1018
+ items.push({ type: "listItem", content: [sub.node] });
1019
+ }
1020
+ i = sub.nextIndex;
1021
+ continue;
1022
+ }
1023
+ if (ordered === null) ordered = m.ordered; // list type set by first item
1024
+ else if (m.ordered !== ordered) break; // marker switched: caller starts a new list
1025
+ const inlineContent = await parseInlineFormatting(m.text, instance);
1026
+ items.push({
1027
+ type: "listItem",
1028
+ content: [{ type: "paragraph", content: inlineContent }],
1029
+ });
1030
+ i++;
1031
+ }
1032
+ return {
1033
+ node: { type: ordered ? "orderedList" : "bulletList", content: items },
1034
+ nextIndex: i,
1035
+ };
1036
+ }
1037
+
968
1038
  // Parse text with markdown formatting and @mentions, build ADF content
969
1039
  async function buildCommentADF(text, instance = defaultInstance) {
970
1040
  // Sanitize: replace em dashes and en dashes with hyphen
@@ -1077,35 +1147,17 @@ async function buildCommentADF(text, instance = defaultInstance) {
1077
1147
  continue;
1078
1148
  }
1079
1149
 
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 });
1150
+ // --- Lists (bullet -,*,+ or ordered 1./1)) with nesting ---
1151
+ const listStart = listItemMatch(line);
1152
+ if (listStart) {
1153
+ const { node, nextIndex } = await parseListBlock(
1154
+ lines,
1155
+ i,
1156
+ listStart.indent,
1157
+ instance,
1158
+ );
1159
+ content.push(node);
1160
+ i = nextIndex;
1109
1161
  continue;
1110
1162
  }
1111
1163
 
@@ -1115,13 +1167,12 @@ async function buildCommentADF(text, instance = defaultInstance) {
1115
1167
  i < lines.length &&
1116
1168
  lines[i].trim() &&
1117
1169
  !lines[i].trim().startsWith("```") &&
1118
- !lines[i].trim().startsWith("# ") &&
1119
- !lines[i].trim().startsWith("## ") &&
1120
- !lines[i].trim().startsWith("### ") &&
1170
+ !/^#{1,6}\s/.test(lines[i].trim()) &&
1121
1171
  !lines[i].trim().startsWith("> ") &&
1122
- !lines[i].trim().startsWith("- ") &&
1123
- !/^\d+\.\s/.test(lines[i].trim()) &&
1172
+ !listItemMatch(lines[i]) &&
1124
1173
  !/^-{3,}$/.test(lines[i].trim()) &&
1174
+ !/^\*{3,}$/.test(lines[i].trim()) &&
1175
+ !/^_{3,}$/.test(lines[i].trim()) &&
1125
1176
  !(lines[i].trim().startsWith("|") && lines[i].trim().endsWith("|"))
1126
1177
  ) {
1127
1178
  if (paragraphContent.length > 0) paragraphContent.push({ type: "hardBreak" });
@@ -1985,9 +2036,20 @@ async function getChangelogsBulk(jql, maxResults = 50, instance = defaultInstanc
1985
2036
 
1986
2037
  // ============ MCP SERVER ============
1987
2038
 
2039
+ const SERVER_INSTRUCTIONS = `# jira-mcp usage rules
2040
+
2041
+ - \`instance\` (optional, on most tools): multi-instance setups only. Omit it — it auto-detects from the issue key prefix, else uses the default instance.
2042
+ - \`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.
2043
+ ${confluenceMode !== "always" ? "- Confluence tools are hidden until activated: call `confluence_enable` first when a task needs Confluence (pages, spaces, page comments, labels, attachments).\n" : ""}
2044
+ ## Writing Jira comments (jira_add_comment / jira_reply_comment / jira_edit_comment)
2045
+ 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.
2046
+
2047
+ ## Writing descriptions (jira_create_ticket / jira_update_ticket / jira_create_subtask)
2048
+ 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.`;
2049
+
1988
2050
  const server = new Server(
1989
2051
  { name: "jira-mcp", version: "1.0.0" },
1990
- { capabilities: { tools: {} } },
2052
+ { capabilities: { tools: { listChanged: true } }, instructions: SERVER_INSTRUCTIONS },
1991
2053
  );
1992
2054
 
1993
2055
  // Inject `dryRun` into the schema of any mutating tool so MCP clients with
@@ -2006,13 +2068,16 @@ function _injectDryRunSchema(tool) {
2006
2068
  ...props,
2007
2069
  dryRun: {
2008
2070
  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.",
2071
+ description: "Preview: return the would-be HTTP request without executing.",
2010
2072
  },
2011
2073
  },
2012
2074
  },
2013
2075
  };
2014
2076
  }
2015
2077
 
2078
+ // Additive formatting guidance moved to SERVER_INSTRUCTIONS to reduce per-tool
2079
+ // token cost. Field descriptions retain their short base text only.
2080
+
2016
2081
  server.setRequestHandler(ListToolsRequestSchema, async () => {
2017
2082
  const rawTools = [
2018
2083
  {
@@ -2024,7 +2089,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2024
2089
  properties: {
2025
2090
  instance: {
2026
2091
  type: "string",
2027
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2092
+ description: "Instance name (optional).",
2028
2093
  },
2029
2094
  },
2030
2095
  required: [],
@@ -2052,7 +2117,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2052
2117
  },
2053
2118
  instance: {
2054
2119
  type: "string",
2055
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2120
+ description: "Instance name (optional).",
2056
2121
  },
2057
2122
  },
2058
2123
  required: ["issueKey"],
@@ -2078,7 +2143,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2078
2143
  },
2079
2144
  instance: {
2080
2145
  type: "string",
2081
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2146
+ description: "Instance name (optional).",
2082
2147
  },
2083
2148
  },
2084
2149
  required: ["jql"],
@@ -2087,7 +2152,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2087
2152
  {
2088
2153
  name: "jira_add_comment",
2089
2154
  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.",
2155
+ "Add a comment to a Jira ticket. Follow the server instructions' comment style rules.",
2091
2156
  inputSchema: {
2092
2157
  type: "object",
2093
2158
  properties: {
@@ -2095,10 +2160,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2095
2160
  type: "string",
2096
2161
  description: "The Jira issue key (e.g., MODS-123)",
2097
2162
  },
2098
- comment: { type: "string", description: "The comment text to add" },
2163
+ comment: {
2164
+ type: "string",
2165
+ description: "The comment text to add.",
2166
+ },
2099
2167
  instance: {
2100
2168
  type: "string",
2101
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2169
+ description: "Instance name (optional).",
2102
2170
  },
2103
2171
  },
2104
2172
  required: ["issueKey", "comment"],
@@ -2120,10 +2188,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2120
2188
  description:
2121
2189
  "The ID of the comment to reply to. Use jira_get_ticket to see comments and their IDs.",
2122
2190
  },
2123
- reply: { type: "string", description: "The reply text" },
2191
+ reply: {
2192
+ type: "string",
2193
+ description: "The reply text.",
2194
+ },
2124
2195
  instance: {
2125
2196
  type: "string",
2126
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2197
+ description: "Instance name (optional).",
2127
2198
  },
2128
2199
  },
2129
2200
  required: ["issueKey", "commentId", "reply"],
@@ -2132,7 +2203,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2132
2203
  {
2133
2204
  name: "jira_edit_comment",
2134
2205
  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.",
2206
+ "Edit an existing comment on a Jira ticket (replaces the text). Follow the server instructions' comment style rules.",
2136
2207
  inputSchema: {
2137
2208
  type: "object",
2138
2209
  properties: {
@@ -2145,10 +2216,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2145
2216
  description:
2146
2217
  "The ID of the comment to edit. Use jira_get_ticket to see comments and their IDs.",
2147
2218
  },
2148
- comment: { type: "string", description: "The new comment text" },
2219
+ comment: {
2220
+ type: "string",
2221
+ description: "The new comment text.",
2222
+ },
2149
2223
  instance: {
2150
2224
  type: "string",
2151
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2225
+ description: "Instance name (optional).",
2152
2226
  },
2153
2227
  },
2154
2228
  required: ["issueKey", "commentId", "comment"],
@@ -2172,7 +2246,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2172
2246
  },
2173
2247
  instance: {
2174
2248
  type: "string",
2175
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2249
+ description: "Instance name (optional).",
2176
2250
  },
2177
2251
  },
2178
2252
  required: ["issueKey", "commentId"],
@@ -2201,7 +2275,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2201
2275
  },
2202
2276
  instance: {
2203
2277
  type: "string",
2204
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2278
+ description: "Instance name (optional).",
2205
2279
  },
2206
2280
  },
2207
2281
  required: ["issueKey"],
@@ -2262,7 +2336,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2262
2336
  },
2263
2337
  instance: {
2264
2338
  type: "string",
2265
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2339
+ description: "Instance name (optional).",
2266
2340
  },
2267
2341
  },
2268
2342
  required: ["issueKey"],
@@ -2286,7 +2360,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2286
2360
  },
2287
2361
  instance: {
2288
2362
  type: "string",
2289
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2363
+ description: "Instance name (optional).",
2290
2364
  },
2291
2365
  },
2292
2366
  required: ["query"],
@@ -2316,7 +2390,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2316
2390
  },
2317
2391
  instance: {
2318
2392
  type: "string",
2319
- description: "Instance name (for multi-instance setups with jql). Auto-detected from issueKey if provided.",
2393
+ description: "Instance name (optional).",
2320
2394
  },
2321
2395
  },
2322
2396
  required: [],
@@ -2408,7 +2482,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2408
2482
  },
2409
2483
  instance: {
2410
2484
  type: "string",
2411
- description: "Instance name. Uses default instance if omitted.",
2485
+ description: "Instance name (optional).",
2412
2486
  },
2413
2487
  },
2414
2488
  required: [],
@@ -2435,7 +2509,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2435
2509
  },
2436
2510
  description: {
2437
2511
  type: "string",
2438
- description: "The issue description text. Supports @mentions via @DisplayName.",
2512
+ description:
2513
+ "The issue description text. Supports @mentions via @DisplayName.",
2439
2514
  },
2440
2515
  assignee: {
2441
2516
  type: "string",
@@ -2478,7 +2553,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2478
2553
  },
2479
2554
  instance: {
2480
2555
  type: "string",
2481
- description: "Instance name override. Auto-detected from project key if omitted.",
2556
+ description: "Instance name (optional).",
2482
2557
  },
2483
2558
  },
2484
2559
  required: ["projectKey", "issueType", "summary"],
@@ -2501,7 +2576,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2501
2576
  },
2502
2577
  description: {
2503
2578
  type: "string",
2504
- description: "The subtask description text. Supports @mentions via @DisplayName.",
2579
+ description:
2580
+ "The subtask description text. Supports @mentions via @DisplayName.",
2505
2581
  },
2506
2582
  assignee: {
2507
2583
  type: "string",
@@ -2522,7 +2598,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2522
2598
  },
2523
2599
  instance: {
2524
2600
  type: "string",
2525
- description: "Instance name override. Auto-detected from parent issue key prefix if omitted.",
2601
+ description: "Instance name (optional).",
2526
2602
  },
2527
2603
  },
2528
2604
  required: ["parentKey", "summary"],
@@ -2553,7 +2629,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2553
2629
  },
2554
2630
  instance: {
2555
2631
  type: "string",
2556
- description: "Instance name override. Auto-detected from inward issue key prefix if omitted.",
2632
+ description: "Instance name (optional).",
2557
2633
  },
2558
2634
  },
2559
2635
  required: ["inwardIssueKey", "outwardIssueKey", "linkType"],
@@ -2576,7 +2652,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2576
2652
  },
2577
2653
  instance: {
2578
2654
  type: "string",
2579
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2655
+ description: "Instance name (optional).",
2580
2656
  },
2581
2657
  },
2582
2658
  required: ["issueKey"],
@@ -2607,7 +2683,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2607
2683
  },
2608
2684
  instance: {
2609
2685
  type: "string",
2610
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2686
+ description: "Instance name (optional).",
2611
2687
  },
2612
2688
  },
2613
2689
  required: ["issueKey"],
@@ -2622,7 +2698,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2622
2698
  properties: {
2623
2699
  instance: {
2624
2700
  type: "string",
2625
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2701
+ description: "Instance name (optional).",
2626
2702
  },
2627
2703
  },
2628
2704
  required: [],
@@ -2641,7 +2717,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2641
2717
  },
2642
2718
  instance: {
2643
2719
  type: "string",
2644
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2720
+ description: "Instance name (optional).",
2645
2721
  },
2646
2722
  },
2647
2723
  required: ["issueKey"],
@@ -2664,7 +2740,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2664
2740
  },
2665
2741
  instance: {
2666
2742
  type: "string",
2667
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2743
+ description: "Instance name (optional).",
2668
2744
  },
2669
2745
  },
2670
2746
  required: ["issueKey", "linkedIssueKey"],
@@ -2691,7 +2767,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2691
2767
  },
2692
2768
  instance: {
2693
2769
  type: "string",
2694
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2770
+ description: "Instance name (optional).",
2695
2771
  },
2696
2772
  },
2697
2773
  required: [],
@@ -2714,7 +2790,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2714
2790
  },
2715
2791
  instance: {
2716
2792
  type: "string",
2717
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2793
+ description: "Instance name (optional).",
2718
2794
  },
2719
2795
  },
2720
2796
  required: ["issueKey", "accountId"],
@@ -2737,7 +2813,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2737
2813
  },
2738
2814
  instance: {
2739
2815
  type: "string",
2740
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2816
+ description: "Instance name (optional).",
2741
2817
  },
2742
2818
  },
2743
2819
  required: ["issueKey", "accountId"],
@@ -2768,7 +2844,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2768
2844
  },
2769
2845
  instance: {
2770
2846
  type: "string",
2771
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2847
+ description: "Instance name (optional).",
2772
2848
  },
2773
2849
  },
2774
2850
  required: ["issueKey", "timeSpent"],
@@ -2787,7 +2863,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2787
2863
  },
2788
2864
  instance: {
2789
2865
  type: "string",
2790
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2866
+ description: "Instance name (optional).",
2791
2867
  },
2792
2868
  },
2793
2869
  required: ["issueKey"],
@@ -2818,7 +2894,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2818
2894
  },
2819
2895
  instance: {
2820
2896
  type: "string",
2821
- description: "Instance name override. Auto-detected from issue key prefix if omitted.",
2897
+ description: "Instance name (optional).",
2822
2898
  },
2823
2899
  },
2824
2900
  required: ["issueKey"],
@@ -2842,7 +2918,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2842
2918
  },
2843
2919
  instance: {
2844
2920
  type: "string",
2845
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2921
+ description: "Instance name (optional).",
2846
2922
  },
2847
2923
  },
2848
2924
  required: ["sprintId", "issueKeys"],
@@ -2865,7 +2941,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2865
2941
  },
2866
2942
  instance: {
2867
2943
  type: "string",
2868
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2944
+ description: "Instance name (optional).",
2869
2945
  },
2870
2946
  },
2871
2947
  required: ["boardId"],
@@ -2892,7 +2968,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2892
2968
  },
2893
2969
  instance: {
2894
2970
  type: "string",
2895
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
2971
+ description: "Instance name (optional).",
2896
2972
  },
2897
2973
  },
2898
2974
  required: [],
@@ -2911,7 +2987,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2911
2987
  },
2912
2988
  instance: {
2913
2989
  type: "string",
2914
- description: "Instance name override. Auto-detected from project key if omitted.",
2990
+ description: "Instance name (optional).",
2915
2991
  },
2916
2992
  },
2917
2993
  required: ["projectKey"],
@@ -2926,7 +3002,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2926
3002
  properties: {
2927
3003
  instance: {
2928
3004
  type: "string",
2929
- description: "Instance name (for multi-instance setups). Uses default instance if omitted.",
3005
+ description: "Instance name (optional).",
2930
3006
  },
2931
3007
  },
2932
3008
  required: [],
@@ -2945,7 +3021,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2945
3021
  },
2946
3022
  instance: {
2947
3023
  type: "string",
2948
- description: "Instance name override. Auto-detected from project key if omitted.",
3024
+ description: "Instance name (optional).",
2949
3025
  },
2950
3026
  },
2951
3027
  required: ["projectKey"],
@@ -2964,7 +3040,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2964
3040
  },
2965
3041
  instance: {
2966
3042
  type: "string",
2967
- description: "Instance name override. Auto-detected from project key if omitted.",
3043
+ description: "Instance name (optional).",
2968
3044
  },
2969
3045
  },
2970
3046
  required: ["projectKey"],
@@ -2979,7 +3055,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2979
3055
  properties: {
2980
3056
  instance: {
2981
3057
  type: "string",
2982
- description: "Instance name. Uses default instance if omitted.",
3058
+ description: "Instance name (optional).",
2983
3059
  },
2984
3060
  },
2985
3061
  required: [],
@@ -2993,7 +3069,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2993
3069
  type: "object",
2994
3070
  properties: {
2995
3071
  spaceKey: { type: "string", description: "Space key (e.g., 'ENG')." },
2996
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3072
+ instance: { type: "string", description: "Instance name (optional)." },
2997
3073
  },
2998
3074
  required: ["spaceKey"],
2999
3075
  },
@@ -3008,7 +3084,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3008
3084
  spaceKey: { type: "string", description: "Unique space key (letters/numbers). Will be uppercased." },
3009
3085
  name: { type: "string", description: "Human-readable space name." },
3010
3086
  description: { type: "string", description: "Optional plain-text description." },
3011
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3087
+ instance: { type: "string", description: "Instance name (optional)." },
3012
3088
  },
3013
3089
  required: ["spaceKey", "name"],
3014
3090
  },
@@ -3023,7 +3099,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3023
3099
  spaceKey: { type: "string", description: "Space key to update." },
3024
3100
  name: { type: "string", description: "New space name." },
3025
3101
  description: { type: "string", description: "New plain-text description." },
3026
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3102
+ instance: { type: "string", description: "Instance name (optional)." },
3027
3103
  },
3028
3104
  required: ["spaceKey"],
3029
3105
  },
@@ -3036,7 +3112,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3036
3112
  type: "object",
3037
3113
  properties: {
3038
3114
  spaceKey: { type: "string", description: "Space key to delete." },
3039
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3115
+ instance: { type: "string", description: "Instance name (optional)." },
3040
3116
  },
3041
3117
  required: ["spaceKey"],
3042
3118
  },
@@ -3052,7 +3128,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3052
3128
  spaceKey: { type: "string", description: "Optional space key to scope the search." },
3053
3129
  type: { type: "string", description: "Content type (default: page). e.g. page, blogpost, comment, attachment." },
3054
3130
  limit: { type: "number", description: "Max results (default: 25)." },
3055
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3131
+ instance: { type: "string", description: "Instance name (optional)." },
3056
3132
  },
3057
3133
  required: ["query"],
3058
3134
  },
@@ -3067,7 +3143,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3067
3143
  spaceKey: { type: "string", description: "Optional space key to scope the query." },
3068
3144
  sinceDays: { type: "number", description: "Days back from now (default: 30)." },
3069
3145
  limit: { type: "number", description: "Max results (default: 25)." },
3070
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3146
+ instance: { type: "string", description: "Instance name (optional)." },
3071
3147
  },
3072
3148
  required: [],
3073
3149
  },
@@ -3081,7 +3157,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3081
3157
  properties: {
3082
3158
  spaceKey: { type: "string", description: "Space key (required)." },
3083
3159
  limit: { type: "number", description: "Max results (default: 50)." },
3084
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3160
+ instance: { type: "string", description: "Instance name (optional)." },
3085
3161
  },
3086
3162
  required: ["spaceKey"],
3087
3163
  },
@@ -3095,7 +3171,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3095
3171
  properties: {
3096
3172
  pageId: { type: "string", description: "Parent page ID (required)." },
3097
3173
  limit: { type: "number", description: "Max results (default: 50)." },
3098
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3174
+ instance: { type: "string", description: "Instance name (optional)." },
3099
3175
  },
3100
3176
  required: ["pageId"],
3101
3177
  },
@@ -3109,7 +3185,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3109
3185
  properties: {
3110
3186
  pageId: { type: "string", description: "Page ID (required)." },
3111
3187
  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." },
3188
+ instance: { type: "string", description: "Instance name (optional)." },
3113
3189
  },
3114
3190
  required: ["pageId"],
3115
3191
  },
@@ -3123,7 +3199,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3123
3199
  properties: {
3124
3200
  pageId: { type: "string", description: "Page ID (required)." },
3125
3201
  limit: { type: "number", description: "Max results (default: 50)." },
3126
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3202
+ instance: { type: "string", description: "Instance name (optional)." },
3127
3203
  },
3128
3204
  required: ["pageId"],
3129
3205
  },
@@ -3138,7 +3214,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3138
3214
  pageId: { type: "string", description: "Page ID (required)." },
3139
3215
  body: { type: "string", description: "Comment body (plain text by default, or storage XHTML if format=storage)." },
3140
3216
  format: { type: "string", description: "'text' (default) or 'storage'." },
3141
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3217
+ instance: { type: "string", description: "Instance name (optional)." },
3142
3218
  },
3143
3219
  required: ["pageId", "body"],
3144
3220
  },
@@ -3154,7 +3230,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3154
3230
  title: { type: "string", description: "New page title (optional — retains current if omitted)." },
3155
3231
  body: { type: "string", description: "New body as storage XHTML (optional — retains current if omitted)." },
3156
3232
  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." },
3233
+ instance: { type: "string", description: "Instance name (optional)." },
3158
3234
  },
3159
3235
  required: ["pageId"],
3160
3236
  },
@@ -3170,7 +3246,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3170
3246
  title: { type: "string", description: "Page title (required)." },
3171
3247
  body: { type: "string", description: "Storage-format XHTML body (required)." },
3172
3248
  parentId: { type: "string", description: "Optional parent page ID to nest under." },
3173
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3249
+ instance: { type: "string", description: "Instance name (optional)." },
3174
3250
  },
3175
3251
  required: ["spaceKey", "title", "body"],
3176
3252
  },
@@ -3182,7 +3258,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3182
3258
  type: "object",
3183
3259
  properties: {
3184
3260
  pageId: { type: "string", description: "Page ID (required)." },
3185
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3261
+ instance: { type: "string", description: "Instance name (optional)." },
3186
3262
  },
3187
3263
  required: ["pageId"],
3188
3264
  },
@@ -3194,7 +3270,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3194
3270
  type: "object",
3195
3271
  properties: {
3196
3272
  pageId: { type: "string", description: "Page ID (required)." },
3197
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3273
+ instance: { type: "string", description: "Instance name (optional)." },
3198
3274
  },
3199
3275
  required: ["pageId"],
3200
3276
  },
@@ -3207,7 +3283,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3207
3283
  properties: {
3208
3284
  pageId: { type: "string", description: "Page ID (required)." },
3209
3285
  label: { type: "string", description: "Label name (required)." },
3210
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3286
+ instance: { type: "string", description: "Instance name (optional)." },
3211
3287
  },
3212
3288
  required: ["pageId", "label"],
3213
3289
  },
@@ -3220,7 +3296,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3220
3296
  properties: {
3221
3297
  pageId: { type: "string", description: "Page ID (required)." },
3222
3298
  label: { type: "string", description: "Label name (required)." },
3223
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3299
+ instance: { type: "string", description: "Instance name (optional)." },
3224
3300
  },
3225
3301
  required: ["pageId", "label"],
3226
3302
  },
@@ -3233,7 +3309,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3233
3309
  properties: {
3234
3310
  pageId: { type: "string", description: "Page ID (required)." },
3235
3311
  limit: { type: "number", description: "Max results (default: 50)." },
3236
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3312
+ instance: { type: "string", description: "Instance name (optional)." },
3237
3313
  },
3238
3314
  required: ["pageId"],
3239
3315
  },
@@ -3250,7 +3326,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3250
3326
  fileContent: { type: "string", description: "Base64-encoded file content (requires fileName)." },
3251
3327
  fileName: { type: "string", description: "File name (required with fileContent; optional with filePath)." },
3252
3328
  comment: { type: "string", description: "Optional attachment comment." },
3253
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3329
+ instance: { type: "string", description: "Instance name (optional)." },
3254
3330
  },
3255
3331
  required: ["pageId"],
3256
3332
  },
@@ -3264,13 +3340,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
3264
3340
  properties: {
3265
3341
  pageId: { type: "string", description: "Page ID (required)." },
3266
3342
  filename: { type: "string", description: "Attachment filename (required)." },
3267
- instance: { type: "string", description: "Instance name. Uses default instance if omitted." },
3343
+ instance: { type: "string", description: "Instance name (optional)." },
3268
3344
  },
3269
3345
  required: ["pageId", "filename"],
3270
3346
  },
3271
3347
  },
3272
3348
  ];
3273
- return { tools: rawTools.map(_injectDryRunSchema) };
3349
+ let tools = rawTools;
3350
+ if (!confluenceActive) {
3351
+ tools = rawTools.filter(t => !t.name.startsWith("confluence_"));
3352
+ tools.push({
3353
+ name: "confluence_enable",
3354
+ 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.",
3355
+ inputSchema: { type: "object", properties: {}, required: [] },
3356
+ });
3357
+ }
3358
+ return { tools: tools.map(_injectDryRunSchema) };
3274
3359
  });
3275
3360
 
3276
3361
  async function runWithPolicy(name, args, handler) {
@@ -3388,6 +3473,19 @@ async function runWithPolicy(name, args, handler) {
3388
3473
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
3389
3474
  const { name, arguments: args } = request.params;
3390
3475
 
3476
+ if (name === "confluence_enable") {
3477
+ const wasActive = confluenceActive;
3478
+ confluenceActive = true;
3479
+ if (!wasActive) server.sendToolListChanged().catch((e) => console.error("jira-mcp: tools/list_changed notification failed:", e?.message || e));
3480
+ return { content: [{ type: "text", text: wasActive ? "Confluence tools were already active." : "Confluence tools activated — the confluence_* tools are now available." }] };
3481
+ }
3482
+ // A client with a cached tool list may call a confluence_* tool directly
3483
+ // while hidden — auto-activate instead of failing.
3484
+ if (!confluenceActive && TOOL_METADATA[name]?.product === "confluence" && name !== "confluence_enable") {
3485
+ confluenceActive = true;
3486
+ server.sendToolListChanged().catch((e) => console.error("jira-mcp: tools/list_changed notification failed:", e?.message || e));
3487
+ }
3488
+
3391
3489
  return runWithPolicy(name, args, async () => {
3392
3490
  if (name === "jira_get_myself") {
3393
3491
  const inst = getInstanceByName(args.instance);
@@ -3509,11 +3607,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3509
3607
  }, inst);
3510
3608
  const author = result.author?.displayName || "Unknown";
3511
3609
  const created = new Date(result.created).toLocaleString();
3610
+ const postedText = quoteText(extractTextSimple(result.body).trim());
3512
3611
  return {
3513
3612
  content: [
3514
3613
  {
3515
3614
  type: "text",
3516
- text: `Comment added to ${args.issueKey} by ${author} at ${created}.`,
3615
+ text: `Comment added to ${args.issueKey} by ${author} at ${created}:\n\n${postedText}`,
3517
3616
  },
3518
3617
  ],
3519
3618
  };
@@ -3577,11 +3676,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3577
3676
  }, inst);
3578
3677
  const author = result.author?.displayName || "Unknown";
3579
3678
  const created = new Date(result.created).toLocaleString();
3679
+ const postedText = quoteText(extractTextSimple(result.body).trim());
3580
3680
  return {
3581
3681
  content: [
3582
3682
  {
3583
3683
  type: "text",
3584
- text: `Reply to ${originalAuthor}'s comment posted on ${args.issueKey} by ${author} at ${created}.`,
3684
+ text: `Reply to ${originalAuthor}'s comment posted on ${args.issueKey} by ${author} at ${created}:\n\n${postedText}`,
3585
3685
  },
3586
3686
  ],
3587
3687
  };
@@ -3601,11 +3701,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3601
3701
  { method: "PUT", body },
3602
3702
  inst,
3603
3703
  );
3704
+ const editedText = quoteText(extractTextSimple(result.body).trim());
3604
3705
  return {
3605
3706
  content: [
3606
3707
  {
3607
3708
  type: "text",
3608
- text: `Comment ${args.commentId} on ${args.issueKey} updated.`,
3709
+ text: `Comment ${args.commentId} on ${args.issueKey} updated:\n\n${editedText}`,
3609
3710
  },
3610
3711
  ],
3611
3712
  };
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.8",
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": {