@crypto512/jicon-mcp 1.1.1 → 1.3.0

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 (79) hide show
  1. package/PROMPT.md +214 -0
  2. package/README.md +77 -8
  3. package/TOOL_LIST.md +319 -101
  4. package/crypto512-jicon-mcp-1.3.0.tgz +0 -0
  5. package/dist/confluence/client.d.ts +7 -2
  6. package/dist/confluence/client.d.ts.map +1 -1
  7. package/dist/confluence/client.js +28 -10
  8. package/dist/confluence/client.js.map +1 -1
  9. package/dist/confluence/tools.d.ts +68 -20
  10. package/dist/confluence/tools.d.ts.map +1 -1
  11. package/dist/confluence/tools.js +682 -131
  12. package/dist/confluence/tools.js.map +1 -1
  13. package/dist/jira/tools.d.ts.map +1 -1
  14. package/dist/jira/tools.js +76 -25
  15. package/dist/jira/tools.js.map +1 -1
  16. package/dist/permissions/tool-registry.d.ts +9 -9
  17. package/dist/permissions/tool-registry.d.ts.map +1 -1
  18. package/dist/permissions/tool-registry.js +10 -3
  19. package/dist/permissions/tool-registry.js.map +1 -1
  20. package/dist/permissions/write-home-validator.d.ts.map +1 -1
  21. package/dist/permissions/write-home-validator.js +23 -3
  22. package/dist/permissions/write-home-validator.js.map +1 -1
  23. package/dist/tempo/client.js +1 -1
  24. package/dist/tempo/client.js.map +1 -1
  25. package/dist/tempo/tools.d.ts.map +1 -1
  26. package/dist/tempo/tools.js +75 -23
  27. package/dist/tempo/tools.js.map +1 -1
  28. package/dist/utils/buffer-tools.d.ts +10 -0
  29. package/dist/utils/buffer-tools.d.ts.map +1 -1
  30. package/dist/utils/buffer-tools.js +139 -28
  31. package/dist/utils/buffer-tools.js.map +1 -1
  32. package/dist/utils/content-buffer.d.ts +5 -1
  33. package/dist/utils/content-buffer.d.ts.map +1 -1
  34. package/dist/utils/content-buffer.js +6 -3
  35. package/dist/utils/content-buffer.js.map +1 -1
  36. package/dist/utils/jicon-help.d.ts +1 -1
  37. package/dist/utils/jicon-help.d.ts.map +1 -1
  38. package/dist/utils/jicon-help.js +158 -78
  39. package/dist/utils/jicon-help.js.map +1 -1
  40. package/dist/utils/plantuml/client.d.ts +15 -1
  41. package/dist/utils/plantuml/client.d.ts.map +1 -1
  42. package/dist/utils/plantuml/client.js +56 -3
  43. package/dist/utils/plantuml/client.js.map +1 -1
  44. package/dist/utils/plantuml/include-expander.d.ts +15 -0
  45. package/dist/utils/plantuml/include-expander.d.ts.map +1 -1
  46. package/dist/utils/plantuml/include-expander.js +47 -8
  47. package/dist/utils/plantuml/include-expander.js.map +1 -1
  48. package/dist/utils/plantuml/index.d.ts +1 -1
  49. package/dist/utils/plantuml/index.d.ts.map +1 -1
  50. package/dist/utils/plantuml/index.js +1 -1
  51. package/dist/utils/plantuml/index.js.map +1 -1
  52. package/dist/utils/plantuml/service.d.ts +1 -1
  53. package/dist/utils/plantuml/service.d.ts.map +1 -1
  54. package/dist/utils/plantuml/service.js +1 -1
  55. package/dist/utils/plantuml/service.js.map +1 -1
  56. package/dist/utils/plantuml/tools.d.ts.map +1 -1
  57. package/dist/utils/plantuml/tools.js +5 -2
  58. package/dist/utils/plantuml/tools.js.map +1 -1
  59. package/dist/utils/response-formatter.d.ts +13 -0
  60. package/dist/utils/response-formatter.d.ts.map +1 -1
  61. package/dist/utils/response-formatter.js +25 -0
  62. package/dist/utils/response-formatter.js.map +1 -1
  63. package/dist/utils/url-tools.d.ts +27 -1
  64. package/dist/utils/url-tools.d.ts.map +1 -1
  65. package/dist/utils/url-tools.js +142 -1
  66. package/dist/utils/url-tools.js.map +1 -1
  67. package/dist/utils/xhtml/index.d.ts +1 -1
  68. package/dist/utils/xhtml/index.d.ts.map +1 -1
  69. package/dist/utils/xhtml/index.js +1 -1
  70. package/dist/utils/xhtml/index.js.map +1 -1
  71. package/dist/utils/xhtml/plantuml.d.ts +24 -6
  72. package/dist/utils/xhtml/plantuml.d.ts.map +1 -1
  73. package/dist/utils/xhtml/plantuml.js +70 -12
  74. package/dist/utils/xhtml/plantuml.js.map +1 -1
  75. package/dist/utils/xhtml/types.d.ts +1 -0
  76. package/dist/utils/xhtml/types.d.ts.map +1 -1
  77. package/dist/utils/xhtml/validator.js +2 -2
  78. package/dist/utils/xhtml/validator.js.map +1 -1
  79. package/package.json +2 -2
@@ -2,12 +2,14 @@
2
2
  * Confluence MCP Tools
3
3
  */
4
4
  import { z } from "zod";
5
- import { formatSuccess, formatError, isApiError } from "../utils/response-formatter.js";
5
+ import { formatSuccess, formatSuccessBuffered, formatError, isApiError } from "../utils/response-formatter.js";
6
6
  import { contentBuffer } from "../utils/content-buffer.js";
7
7
  import { formatPageMetadata } from "./formatters.js";
8
8
  import { validateXhtmlAsync, parseXhtml, parseStructure, serializeXhtml, enhanceXhtmlError } from "../utils/xhtml/index.js";
9
9
  import { detectRawPlantUml, detectDiagramType } from "../utils/xhtml/plantuml.js";
10
- import { expandPlantUmlInXhtml } from "../utils/plantuml/index.js";
10
+ import { expandPlantUmlInXhtml, collapseExpandedIncludesInXhtml } from "../utils/plantuml/index.js";
11
+ import { parseUrl } from "../utils/url-tools.js";
12
+ import { DEFAULT_PAGE_EXPAND } from "./defaults.js";
11
13
  /**
12
14
  * Generate a summary of content structure for draft responses.
13
15
  * Helps AI assistants verify that diagrams and other elements are included.
@@ -44,72 +46,17 @@ function getContentSummary(content) {
44
46
  result.headingCount = doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
45
47
  return result;
46
48
  }
47
- /**
48
- * Resolve content from either direct content string or bufferId.
49
- * Returns { content, error } - if error is set, return it as the tool result.
50
- */
51
- function resolveContentFromBuffer(contentArg, bufferIdArg) {
52
- // Exactly one of content or bufferId must be provided
53
- if (contentArg && bufferIdArg) {
54
- return {
55
- error: formatError({
56
- error: true,
57
- message: "Provide either 'content' or 'bufferId', not both",
58
- statusCode: 400,
59
- }),
60
- };
61
- }
62
- if (bufferIdArg) {
63
- const bufferChunk = contentBuffer.getChunk(bufferIdArg);
64
- if (!bufferChunk) {
65
- return {
66
- error: formatError({
67
- error: true,
68
- message: `Buffer ${bufferIdArg} not found or expired`,
69
- statusCode: 404,
70
- }),
71
- };
72
- }
73
- // Validate that buffer contains XHTML content for Confluence
74
- const bufferInfo = contentBuffer.getInfo(bufferIdArg);
75
- if (bufferInfo?.metadata?.contentType && bufferInfo.metadata.contentType !== "xhtml") {
76
- return {
77
- error: formatError({
78
- error: true,
79
- message: `Buffer ${bufferIdArg} contains ${bufferInfo.metadata.contentType} content, but Confluence requires XHTML.`,
80
- statusCode: 400,
81
- details: {
82
- hint: "Create Confluence content with buffer_create, then edit with buffer_edit.",
83
- foundContentType: bufferInfo.metadata.contentType,
84
- expectedContentType: "xhtml",
85
- },
86
- }),
87
- };
88
- }
89
- // Get full content from buffer
90
- const fullContent = contentBuffer.getChunk(bufferIdArg, 0, bufferChunk.totalSize);
91
- if (!fullContent) {
92
- return {
93
- error: formatError({
94
- error: true,
95
- message: "Failed to retrieve full buffer content",
96
- statusCode: 500,
97
- }),
98
- };
99
- }
100
- return { content: fullContent.chunk };
101
- }
102
- // Direct content provided
103
- return { content: contentArg };
104
- }
105
49
  /**
106
50
  * Validate XHTML content before Confluence write operations.
107
51
  * Returns error result if validation fails, null if valid.
52
+ * When bufferId is provided, includes it in error response for recovery.
108
53
  */
109
- async function validateContentForWrite(content) {
54
+ async function validateContentForWrite(content, bufferId) {
110
55
  const validation = await validateXhtmlAsync(content, { validatePlantUml: true });
111
56
  if (!validation.valid) {
112
57
  const errorMessages = [];
58
+ let errorElementId;
59
+ let errorContext;
113
60
  // XHTML structure errors
114
61
  if (validation.errors && validation.errors.length > 0) {
115
62
  errorMessages.push("XHTML validation errors:");
@@ -122,6 +69,14 @@ async function validateContentForWrite(content) {
122
69
  ? err.location.context.substring(0, 60) + "..."
123
70
  : err.location.context;
124
71
  errorMessages.push(` Near: "${contextPreview}"`);
72
+ // Capture first error context for recovery
73
+ if (!errorContext) {
74
+ errorContext = err.location.context;
75
+ }
76
+ }
77
+ // Capture first error elementId for recovery
78
+ if (!errorElementId && err.location?.elementId) {
79
+ errorElementId = err.location.elementId;
125
80
  }
126
81
  });
127
82
  }
@@ -149,10 +104,18 @@ async function validateContentForWrite(content) {
149
104
  errorMessages.push(" </ac:structured-macro>");
150
105
  }
151
106
  }
152
- // Add help hints
107
+ // Add recovery instructions
153
108
  errorMessages.push("");
154
- errorMessages.push('TIP: Call help(topic="storage") for Confluence XHTML format guide.');
155
- errorMessages.push('TIP: Call help(topic="plantuml") for PlantUML macro examples.');
109
+ if (bufferId) {
110
+ errorMessages.push(`RECOVERY: Use buffer_edit(bufferId="${bufferId}", ...) to fix errors.`);
111
+ if (errorElementId) {
112
+ errorMessages.push(` buffer_edit(bufferId="${bufferId}", replace=${errorElementId}, content="<fixed>...</fixed>")`);
113
+ }
114
+ else if (errorContext) {
115
+ errorMessages.push(` Use buffer_grep(bufferId="${bufferId}", pattern="...") to find the error location.`);
116
+ }
117
+ }
118
+ errorMessages.push('TIP: Call help(topic="storage") for XHTML syntax (HTML vs XHTML differences).');
156
119
  errorMessages.push("");
157
120
  errorMessages.push("ACTION REQUIRED: Fix content errors before calling this tool again.");
158
121
  errorMessages.push("DO NOT claim success - the draft was NOT created.");
@@ -162,7 +125,12 @@ async function validateContentForWrite(content) {
162
125
  statusCode: 400,
163
126
  details: {
164
127
  validationErrors: errorMessages,
165
- hint: "Use buffer_validate_xhtml to check content, or plantuml_validate for PlantUML syntax",
128
+ ...(bufferId && { bufferId }),
129
+ ...(errorElementId && { errorElementId }),
130
+ ...(errorContext && { errorContext }),
131
+ hint: errorElementId
132
+ ? `Use buffer_edit(bufferId="${bufferId}", replace=${errorElementId}, content="...") to fix`
133
+ : `Use buffer_grep to find the error, then buffer_edit to fix`,
166
134
  },
167
135
  });
168
136
  }
@@ -183,7 +151,9 @@ function storeXhtmlWithStructure(content, metadata) {
183
151
  export function createConfluenceTools(client) {
184
152
  return {
185
153
  confluence_search_content: {
186
- description: `Search Confluence content using CQL. Auto-fetches all results (up to 5000).
154
+ description: `Search Confluence content using CQL. Auto-fetches all results.
155
+
156
+ Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.
187
157
 
188
158
  TIP: See help(topic="cql") for CQL syntax guide.
189
159
 
@@ -197,8 +167,10 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
197
167
  handler: async (args) => {
198
168
  try {
199
169
  const result = await client.searchContentAll(args.cql, args.expand);
200
- // Use formatSuccess for automatic buffering of large responses
201
- return formatSuccess(result);
170
+ return formatSuccessBuffered(result, {
171
+ resourceType: "confluence_search",
172
+ title: `CQL: ${args.cql.substring(0, 100)}${args.cql.length > 100 ? "..." : ""}`,
173
+ });
202
174
  }
203
175
  catch (error) {
204
176
  // Enhanced error handling for common CQL errors
@@ -225,7 +197,7 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
225
197
  hint += "- ✅ RIGHT: (text~\"a\" OR text~\"b\") - Each term needs its own text~\n";
226
198
  }
227
199
  hint += "\nExamples:\n";
228
- hint += " text~\"Nicolas Pernoud\" - finds pages mentioning this person\n";
200
+ hint += " text~\"Mike Tasc\" - finds pages mentioning this person\n";
229
201
  hint += " text~\"meeting\" AND space=MESH - finds meetings in MESH space\n";
230
202
  hint += " title~\"sprint review\" - finds pages with sprint review in title";
231
203
  return formatError({
@@ -256,7 +228,7 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
256
228
  PREFERRED method when you have a page ID from search results. Faster and more reliable than get_page_by_title.
257
229
 
258
230
  Returns pageId, version, bufferId, and structure (element IDs) for structured editing.
259
- Use buffer_edit with element IDs to modify, then confluence_draft_create for user review.`,
231
+ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.`,
260
232
  inputSchema: z.object({
261
233
  pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
262
234
  expand: z
@@ -267,7 +239,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
267
239
  handler: async (args) => {
268
240
  try {
269
241
  const result = await client.getPage(args.pageId, args.expand);
270
- const content = result.body?.storage?.value || "";
242
+ const rawContent = result.body?.storage?.value || "";
243
+ // Collapse expanded includes back to !include directives
244
+ const content = collapseExpandedIncludesInXhtml(rawContent);
271
245
  // Store content with element IDs for structured editing
272
246
  const { bufferId, structure } = storeXhtmlWithStructure(content, {
273
247
  resourceType: "confluence_page",
@@ -284,7 +258,7 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
284
258
  bufferId,
285
259
  ...(structure && { structure }),
286
260
  contentSize: content.length,
287
- message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create for user review.",
261
+ message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
288
262
  });
289
263
  }
290
264
  catch (error) {
@@ -298,7 +272,7 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
298
272
  Use ONLY when you don't have a page ID. IMPORTANT: Use the space KEY (e.g. 'MESH', 'TC'), NOT the space name.
299
273
 
300
274
  Returns pageId, version, bufferId, and structure (element IDs) for structured editing.
301
- Use buffer_edit with element IDs to modify, then confluence_draft_create for user review.`,
275
+ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.`,
302
276
  inputSchema: z.object({
303
277
  spaceKey: z.string().describe("Space key (short code like 'MESH', 'TC'), NOT the full space name"),
304
278
  title: z.string().describe("Page title"),
@@ -314,7 +288,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
314
288
  statusCode: 404,
315
289
  });
316
290
  }
317
- const content = result.body?.storage?.value || "";
291
+ const rawContent = result.body?.storage?.value || "";
292
+ // Collapse expanded includes back to !include directives
293
+ const content = collapseExpandedIncludesInXhtml(rawContent);
318
294
  // Store content with element IDs for structured editing
319
295
  const { bufferId, structure } = storeXhtmlWithStructure(content, {
320
296
  resourceType: "confluence_page",
@@ -331,7 +307,196 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
331
307
  bufferId,
332
308
  ...(structure && { structure }),
333
309
  contentSize: content.length,
334
- message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create for user review.",
310
+ message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
311
+ });
312
+ }
313
+ catch (error) {
314
+ return formatError(isApiError(error) ? error : new Error(String(error)));
315
+ }
316
+ },
317
+ },
318
+ confluence_edit: {
319
+ description: `Smart page/draft loader - auto-resolves URLs, pageIds, draftIds, or SPACE/Title.
320
+
321
+ ACCEPTS ANY OF:
322
+ - Full URL: https://confluence.example.com/pages/viewpage.action?pageId=123
323
+ - Full URL: https://confluence.example.com/pages/resumedraft.action?draftId=456
324
+ - Full URL: https://confluence.example.com/display/SPACE/Page+Title
325
+ - Page ID: "123456"
326
+ - Draft ID: "draft:123456" (prefix with "draft:")
327
+ - Space/Title: "DOCS/API Guide"
328
+
329
+ SMART BEHAVIOR:
330
+ - URLs are parsed automatically to extract pageId or draftId
331
+ - Draft IDs: tries to load draft; if 404 (published), finds page by title
332
+ - Returns bufferId + structure + pageId for editing
333
+
334
+ WORKFLOW:
335
+ 1. confluence_edit(input) → bufferId, structure, pageId
336
+ 2. buffer_edit(bufferId, ...) → modify content
337
+ 3. confluence_draft_create(pageId=..., bufferId=...) → draft linked to original page
338
+ 4. User publishes via Confluence UI (updates original page)
339
+ 5. For more edits: confluence_edit(same URL or "SPACE/Title") → auto-resolves`,
340
+ inputSchema: z.object({
341
+ input: z.string().describe('URL, pageId, "draft:ID", or "SPACE/Title"'),
342
+ }),
343
+ handler: async (args) => {
344
+ const input = args.input.trim();
345
+ if (!input) {
346
+ return formatError({
347
+ error: true,
348
+ message: "Input is required. Provide a URL, pageId, draft:ID, or SPACE/Title.",
349
+ statusCode: 400,
350
+ });
351
+ }
352
+ // Helper to load page content and return formatted result
353
+ const loadPageContent = async (pageResult) => {
354
+ const rawContent = pageResult.body?.storage?.value || "";
355
+ const content = collapseExpandedIncludesInXhtml(rawContent);
356
+ const { bufferId, structure } = storeXhtmlWithStructure(content, {
357
+ resourceType: "confluence_page",
358
+ resourceId: String(pageResult.id),
359
+ contentType: "xhtml",
360
+ version: pageResult.version?.number,
361
+ spaceKey: pageResult.space?.key,
362
+ title: pageResult.title,
363
+ });
364
+ return {
365
+ pageId: pageResult.id,
366
+ spaceKey: pageResult.space?.key,
367
+ title: pageResult.title,
368
+ version: pageResult.version?.number,
369
+ bufferId,
370
+ structure,
371
+ contentSize: content.length,
372
+ message: "Content loaded. Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
373
+ };
374
+ };
375
+ // Helper to load draft content
376
+ const loadDraftContent = async (draftId) => {
377
+ const result = await client.getDraft(draftId);
378
+ const rawContent = result.body?.storage?.value || "";
379
+ const content = collapseExpandedIncludesInXhtml(rawContent);
380
+ const { bufferId, structure } = storeXhtmlWithStructure(content, {
381
+ resourceType: "confluence_page",
382
+ resourceId: String(result.id),
383
+ contentType: "xhtml",
384
+ spaceKey: result.space?.key,
385
+ title: result.title,
386
+ isDraft: true,
387
+ });
388
+ return {
389
+ draftId: result.id,
390
+ spaceKey: result.space?.key,
391
+ title: result.title,
392
+ bufferId,
393
+ structure,
394
+ contentSize: content.length,
395
+ message: "Draft loaded. Use buffer_edit to modify, then confluence_draft_save to update.",
396
+ };
397
+ };
398
+ try {
399
+ // Case 1: Full URL
400
+ if (input.startsWith("http://") || input.startsWith("https://")) {
401
+ const parsed = parseUrl(input);
402
+ if (parsed.type === "confluence_page" && parsed.pageId) {
403
+ const result = await client.getPage(parsed.pageId);
404
+ return formatSuccess(await loadPageContent(result));
405
+ }
406
+ if (parsed.type === "confluence_draft" && parsed.draftId) {
407
+ try {
408
+ return formatSuccess(await loadDraftContent(parsed.draftId));
409
+ }
410
+ catch (error) {
411
+ // Draft may have been published - try to find page by title
412
+ if (isApiError(error) && error.statusCode === 404 && parsed.spaceKey && parsed.title) {
413
+ const pageResult = await client.getPageByTitle(parsed.spaceKey, parsed.title);
414
+ if (pageResult) {
415
+ return formatSuccess({
416
+ ...await loadPageContent(pageResult),
417
+ note: "Draft was published. Loaded the published page instead.",
418
+ });
419
+ }
420
+ }
421
+ throw error;
422
+ }
423
+ }
424
+ if (parsed.type === "confluence_space_path" && parsed.spaceKey && parsed.title) {
425
+ const pageResult = await client.getPageByTitle(parsed.spaceKey, parsed.title);
426
+ if (!pageResult) {
427
+ return formatError({
428
+ error: true,
429
+ message: `Page not found: "${parsed.title}" in space ${parsed.spaceKey}`,
430
+ statusCode: 404,
431
+ });
432
+ }
433
+ return formatSuccess(await loadPageContent(pageResult));
434
+ }
435
+ return formatError({
436
+ error: true,
437
+ message: `Unable to parse URL: "${input}". Use Confluence page, draft, or display URL.`,
438
+ statusCode: 400,
439
+ });
440
+ }
441
+ // Case 2: Draft ID (prefix with "draft:")
442
+ if (input.startsWith("draft:")) {
443
+ const draftId = input.substring(6);
444
+ try {
445
+ return formatSuccess(await loadDraftContent(draftId));
446
+ }
447
+ catch (error) {
448
+ if (isApiError(error) && error.statusCode === 404) {
449
+ return formatError({
450
+ error: true,
451
+ message: `Draft ${draftId} not found. It may have been published. Try using the page URL or "SPACE/Title" format.`,
452
+ statusCode: 404,
453
+ details: {
454
+ hint: "If the draft was published, ask the user for the published page URL.",
455
+ },
456
+ });
457
+ }
458
+ throw error;
459
+ }
460
+ }
461
+ // Case 3: Space/Title format (e.g., "DOCS/API Guide")
462
+ if (input.includes("/") && !input.match(/^\d+$/)) {
463
+ const slashIndex = input.indexOf("/");
464
+ const spaceKey = input.substring(0, slashIndex).trim();
465
+ const title = input.substring(slashIndex + 1).trim();
466
+ if (!spaceKey || !title) {
467
+ return formatError({
468
+ error: true,
469
+ message: `Invalid SPACE/Title format: "${input}". Expected "SPACEKEY/Page Title".`,
470
+ statusCode: 400,
471
+ });
472
+ }
473
+ const pageResult = await client.getPageByTitle(spaceKey, title);
474
+ if (!pageResult) {
475
+ return formatError({
476
+ error: true,
477
+ message: `Page not found: "${title}" in space ${spaceKey}`,
478
+ statusCode: 404,
479
+ });
480
+ }
481
+ return formatSuccess(await loadPageContent(pageResult));
482
+ }
483
+ // Case 4: Pure numeric page ID
484
+ if (/^\d+$/.test(input)) {
485
+ const result = await client.getPage(input);
486
+ return formatSuccess(await loadPageContent(result));
487
+ }
488
+ return formatError({
489
+ error: true,
490
+ message: `Unable to parse input: "${input}". Expected URL, pageId, "draft:ID", or "SPACE/Title".`,
491
+ statusCode: 400,
492
+ details: {
493
+ examples: [
494
+ "https://confluence.example.com/pages/viewpage.action?pageId=123",
495
+ "123456 (page ID)",
496
+ "draft:456789 (draft ID)",
497
+ "DOCS/API Guide (space/title)",
498
+ ],
499
+ },
335
500
  });
336
501
  }
337
502
  catch (error) {
@@ -366,7 +531,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
366
531
  },
367
532
  },
368
533
  confluence_list_spaces: {
369
- description: `List all accessible Confluence spaces.`,
534
+ description: `List all accessible Confluence spaces.
535
+
536
+ Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
370
537
  inputSchema: z.object({
371
538
  type: z
372
539
  .enum(["global", "personal"])
@@ -376,7 +543,10 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
376
543
  handler: async (args) => {
377
544
  try {
378
545
  const result = await client.listSpaces(args.type);
379
- return formatSuccess(result);
546
+ return formatSuccessBuffered(result, {
547
+ resourceType: "confluence_spaces",
548
+ title: args.type ? `${args.type} spaces` : "All Spaces",
549
+ });
380
550
  }
381
551
  catch (error) {
382
552
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -384,7 +554,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
384
554
  },
385
555
  },
386
556
  confluence_get_space: {
387
- description: "Get detailed information about a space",
557
+ description: `Get detailed information about a space.
558
+
559
+ Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
388
560
  inputSchema: z.object({
389
561
  spaceKey: z.string().describe("Space key"),
390
562
  expand: z.array(z.string()).optional().describe("Additional data to expand"),
@@ -392,8 +564,11 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
392
564
  handler: async (args) => {
393
565
  try {
394
566
  const result = await client.getSpace(args.spaceKey, args.expand);
395
- // Use formatSuccess for automatic buffering of large responses
396
- return formatSuccess(result);
567
+ return formatSuccessBuffered(result, {
568
+ resourceType: "confluence_space",
569
+ title: result.name || args.spaceKey,
570
+ spaceKey: args.spaceKey,
571
+ });
397
572
  }
398
573
  catch (error) {
399
574
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -401,7 +576,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
401
576
  },
402
577
  },
403
578
  confluence_get_page_children: {
404
- description: `Get all child pages of a page.`,
579
+ description: `Get all child pages of a page.
580
+
581
+ Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
405
582
  inputSchema: z.object({
406
583
  pageId: z.coerce.string().describe("Parent page ID (accepts string or number)"),
407
584
  expand: z.array(z.string()).optional().describe("Additional data to expand"),
@@ -409,7 +586,11 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
409
586
  handler: async (args) => {
410
587
  try {
411
588
  const result = await client.getPageChildren(args.pageId, args.expand);
412
- return formatSuccess(result);
589
+ return formatSuccessBuffered(result, {
590
+ resourceType: "confluence_page_children",
591
+ title: `Page ${args.pageId} children`,
592
+ pageId: args.pageId,
593
+ });
413
594
  }
414
595
  catch (error) {
415
596
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -433,14 +614,20 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
433
614
  },
434
615
  },
435
616
  confluence_get_comments: {
436
- description: `Get all comments on a Confluence page.`,
617
+ description: `Get all comments on a Confluence page.
618
+
619
+ Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
437
620
  inputSchema: z.object({
438
621
  pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
439
622
  }),
440
623
  handler: async (args) => {
441
624
  try {
442
625
  const result = await client.getComments(args.pageId);
443
- return formatSuccess(result);
626
+ return formatSuccessBuffered(result, {
627
+ resourceType: "confluence_comments",
628
+ title: `Page ${args.pageId} comments`,
629
+ pageId: args.pageId,
630
+ });
444
631
  }
445
632
  catch (error) {
446
633
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -465,14 +652,20 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
465
652
  },
466
653
  },
467
654
  confluence_list_attachments: {
468
- description: `List all attachments on a Confluence page.`,
655
+ description: `List all attachments on a Confluence page.
656
+
657
+ Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
469
658
  inputSchema: z.object({
470
659
  pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
471
660
  }),
472
661
  handler: async (args) => {
473
662
  try {
474
663
  const result = await client.listAttachments(args.pageId);
475
- return formatSuccess(result);
664
+ return formatSuccessBuffered(result, {
665
+ resourceType: "confluence_attachments",
666
+ title: `Page ${args.pageId} attachments`,
667
+ pageId: args.pageId,
668
+ });
476
669
  }
477
670
  catch (error) {
478
671
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -521,44 +714,126 @@ Returns the user's personal space key and details. Use this to verify your perso
521
714
  confluence_draft_create: {
522
715
  description: `Create a Confluence draft for user review. Returns draftId, bufferId, structure (element IDs), and clickable URL.
523
716
 
524
- IMPORTANT: User must validate the draft in Confluence UI before publishing.
525
- IMPORTANT: Raw @startuml outside macros is NOT supported. Use buffer_edit for auto-wrapping, or embed properly formatted ac:structured-macro elements.
717
+ REQUIRES bufferId - content must be in a buffer for validation and error recovery.
718
+
719
+ Workflow for NEW page:
720
+ 1. buffer_create(content="<h1>Title</h1><p>Content</p>", contentType="xhtml") → bufferId, structure
721
+ 2. buffer_validate_xhtml(bufferId) → check for errors, get elementId if invalid
722
+ 3. buffer_edit(bufferId, replace=elementId, content="...") → fix errors if any
723
+ 4. confluence_draft_create(spaceKey, title, bufferId) → creates draft for review
526
724
 
527
- TIP: See help(topic="storage") for XHTML syntax, help(topic="plantuml") for diagrams.
725
+ Workflow for EDITING existing page:
726
+ 1. confluence_get_page(pageId) or confluence_edit(input) → bufferId, structure
727
+ 2. buffer_edit(bufferId, after=ID, content/plantuml) → modify content
728
+ 3. buffer_validate_xhtml(bufferId) → check for errors
729
+ 4. confluence_draft_create(pageId, bufferId) → creates "[jicon-mcp REVIEW] Title" draft
730
+ 5. User reviews in Confluence UI
731
+ 6. confluence_review_publish(draftId) → applies changes to original page
528
732
 
529
- Workflow:
530
- 1. Create draft with this tool → returns URL and structure with element IDs
531
- 2. User reviews draft in Confluence UI (can edit there)
532
- 3. If user requests changes: use confluence_draft_open to get user's edits
533
- 4. Modify with buffer_edit using element IDs, then confluence_draft_save for new review
534
- 5. User publishes manually via Confluence UI
733
+ On validation error: returns bufferId + errorElementId for surgical fix with buffer_edit.
535
734
 
536
- Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit).`,
735
+ IMPORTANT: Call help(topic="storage") for XHTML syntax (HTML vs XHTML differences).
736
+ IMPORTANT: Call help(topic="plantuml") for diagram syntax.`,
537
737
  inputSchema: z.object({
538
- spaceKey: z.string().describe("Space key"),
539
- title: z.string().describe("Page title"),
540
- content: z
541
- .string()
542
- .optional()
543
- .describe("Page content in Confluence storage format (XHTML-based)"),
544
- bufferId: z.string().optional().describe("Buffer ID containing content (alternative to content)"),
738
+ pageId: z.coerce.string().optional().describe("Existing page ID to create edit draft for. When provided, bufferId must come from that page."),
739
+ spaceKey: z.string().optional().describe("Space key (required for new pages, auto-populated when pageId is provided)"),
740
+ title: z.string().optional().describe("Page title (required for new pages, auto-populated when pageId is provided)"),
741
+ bufferId: z.string().describe("Buffer ID containing XHTML content (from buffer_create or confluence_get_page)"),
545
742
  parentId: z.string().optional().describe("Parent page ID"),
546
743
  labels: z.array(z.string()).optional().describe("Array of labels"),
547
744
  }),
548
745
  handler: async (args) => {
549
- // Resolve content from either content string or bufferId
550
- const resolved = resolveContentFromBuffer(args.content, args.bufferId);
551
- if (resolved.error) {
552
- return resolved.error;
746
+ // Determine the mode: editing existing page vs creating new draft
747
+ let spaceKey = args.spaceKey;
748
+ let title = args.title;
749
+ let parentId = args.parentId;
750
+ let originalPageId;
751
+ let originalPageVersion;
752
+ // Validate buffer exists and get content
753
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
754
+ if (!bufferInfo) {
755
+ return formatError({
756
+ error: true,
757
+ message: `Buffer not found or expired: ${args.bufferId}`,
758
+ statusCode: 404,
759
+ details: {
760
+ hint: "Create a buffer first: buffer_create(content='<h1>...</h1>', contentType='xhtml')",
761
+ },
762
+ });
553
763
  }
554
- if (!resolved.content) {
764
+ // Validate buffer contains XHTML content
765
+ if (bufferInfo.metadata?.contentType && bufferInfo.metadata.contentType !== "xhtml") {
555
766
  return formatError({
556
767
  error: true,
557
- message: "Either 'content' or 'bufferId' must be provided",
768
+ message: `Buffer '${args.bufferId}' is not XHTML content (found: ${bufferInfo.metadata.contentType})`,
558
769
  statusCode: 400,
770
+ details: {
771
+ hint: "Use buffer_create(content='...', contentType='xhtml') to create an XHTML buffer",
772
+ },
559
773
  });
560
774
  }
561
- const content = resolved.content;
775
+ // Get full content from buffer
776
+ const fullContent = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
777
+ if (!fullContent) {
778
+ return formatError({
779
+ error: true,
780
+ message: "Failed to retrieve buffer content",
781
+ statusCode: 500,
782
+ });
783
+ }
784
+ const content = fullContent.chunk;
785
+ if (args.pageId) {
786
+ // MODE: Edit existing page - validate bufferId came from this page
787
+ const bufferSourceId = bufferInfo.metadata?.resourceId;
788
+ if (bufferSourceId !== args.pageId) {
789
+ return formatError({
790
+ error: true,
791
+ message: `Buffer '${args.bufferId}' does not belong to page '${args.pageId}'`,
792
+ statusCode: 400,
793
+ details: {
794
+ bufferSource: bufferSourceId || "unknown",
795
+ expectedPage: args.pageId,
796
+ hint: "Two options: (1) Load page first with confluence_edit(pageId) then use buffer_edit to modify, OR (2) If you already created content in a separate buffer, use buffer_edit(pageBufferId, after=N, fromBufferId=yourNewBuffer) to merge it into the page buffer",
797
+ },
798
+ });
799
+ }
800
+ // Fetch original page info to get space, title, parent, etc.
801
+ try {
802
+ const originalPage = await client.getPage(args.pageId, ["space", "ancestors", "version"]);
803
+ spaceKey = originalPage.space?.key;
804
+ title = args.title || originalPage.title; // Allow title override
805
+ originalPageId = originalPage.id;
806
+ originalPageVersion = originalPage.version?.number;
807
+ // Get parent from ancestors if not explicitly provided
808
+ if (!parentId && originalPage.ancestors && originalPage.ancestors.length > 0) {
809
+ parentId = originalPage.ancestors[originalPage.ancestors.length - 1].id;
810
+ }
811
+ if (!spaceKey) {
812
+ return formatError({
813
+ error: true,
814
+ message: `Could not determine space key for page ${args.pageId}`,
815
+ statusCode: 400,
816
+ });
817
+ }
818
+ }
819
+ catch (error) {
820
+ return formatError({
821
+ error: true,
822
+ message: `Failed to fetch original page ${args.pageId}: ${error instanceof Error ? error.message : String(error)}`,
823
+ statusCode: 404,
824
+ });
825
+ }
826
+ }
827
+ else {
828
+ // MODE: New page - require spaceKey and title
829
+ if (!spaceKey || !title) {
830
+ return formatError({
831
+ error: true,
832
+ message: "For new drafts, both 'spaceKey' and 'title' are required. For editing existing pages, provide 'pageId' instead.",
833
+ statusCode: 400,
834
+ });
835
+ }
836
+ }
562
837
  // Check for raw PlantUML that should use buffer_edit with plantuml parameter
563
838
  const rawPlantUml = detectRawPlantUml(content);
564
839
  if (rawPlantUml) {
@@ -573,21 +848,73 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
573
848
  });
574
849
  }
575
850
  // Validate XHTML and PlantUML content before writing
576
- const validationError = await validateContentForWrite(content);
851
+ const validationError = await validateContentForWrite(content, args.bufferId);
577
852
  if (validationError) {
578
853
  return validationError;
579
854
  }
580
855
  // Expand PlantUML !include directives before sending to Confluence
581
856
  const expandedContent = await expandPlantUmlInXhtml(content);
582
857
  try {
583
- const result = await client.createDraft({
584
- spaceKey: args.spaceKey,
585
- title: args.title,
586
- content: expandedContent,
587
- parentId: args.parentId,
588
- labels: args.labels,
589
- });
858
+ // Constants for review workflow
859
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
860
+ const REVIEW_LABEL_PREFIX = 'jicon-review-';
861
+ let result;
862
+ let existingReviewDraft = null;
863
+ let reviewLabel;
864
+ if (originalPageId) {
865
+ // REVIEW WORKFLOW: Creating/updating review draft for existing page
866
+ reviewLabel = `${REVIEW_LABEL_PREFIX}${originalPageId}`;
867
+ // Check if review draft already exists for this page
868
+ existingReviewDraft = await client.findDraftByLabel(reviewLabel);
869
+ if (existingReviewDraft) {
870
+ // Delete existing review draft to update it (API doesn't support draft updates)
871
+ await client.deleteDraft(existingReviewDraft.id);
872
+ }
873
+ // Prevent nested review drafts
874
+ const reviewTitle = title.startsWith(REVIEW_PREFIX)
875
+ ? title
876
+ : `${REVIEW_PREFIX}${title}`;
877
+ // Create review draft with label linking to original page
878
+ const labels = [...(args.labels || []), reviewLabel];
879
+ result = await client.createDraft({
880
+ spaceKey: spaceKey,
881
+ title: reviewTitle,
882
+ content: expandedContent,
883
+ parentId: parentId,
884
+ labels: labels,
885
+ });
886
+ }
887
+ else {
888
+ // NEW PAGE MODE: Create standalone draft
889
+ result = await client.createDraft({
890
+ spaceKey: spaceKey,
891
+ title: title,
892
+ content: expandedContent,
893
+ parentId: parentId,
894
+ labels: args.labels,
895
+ });
896
+ }
897
+ // Verify draft is readable with labels (catches label attachment failures)
898
+ if (originalPageId && reviewLabel) {
899
+ const verifyDraft = await client.getDraft(result.id, [
900
+ ...DEFAULT_PAGE_EXPAND,
901
+ "metadata.labels",
902
+ ]);
903
+ const labels = verifyDraft.metadata?.labels?.results || [];
904
+ const hasReviewLabel = labels.some((l) => l.name === reviewLabel);
905
+ if (!hasReviewLabel) {
906
+ // Label attachment may have failed - try to add it again
907
+ // This can happen due to eventual consistency or API timing
908
+ try {
909
+ await client.addLabels(result.id, [reviewLabel], true);
910
+ }
911
+ catch {
912
+ // If label add fails, warn but continue (draft still exists)
913
+ }
914
+ }
915
+ }
590
916
  // Store content with element IDs for structured editing
917
+ // Track originalPageId in metadata so we can link the draft to its source
591
918
  const { bufferId: newBufferId } = storeXhtmlWithStructure(content, {
592
919
  resourceType: "confluence_page",
593
920
  resourceId: result.id,
@@ -595,13 +922,16 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
595
922
  spaceKey: result.space?.key,
596
923
  title: result.title,
597
924
  isDraft: true,
925
+ originalPageId: originalPageId,
926
+ originalPageVersion: originalPageVersion,
598
927
  });
599
928
  // Build the draft edit URL (drafts use resumedraft.action, not the webui link)
600
929
  const baseUrl = client.getBaseUrl();
601
930
  const draftUrl = `${baseUrl}/pages/resumedraft.action?draftId=${result.id}`;
602
931
  // Generate content summary for verification
603
932
  const summary = getContentSummary(content);
604
- return formatSuccess({
933
+ // Build response with original page info if editing existing page
934
+ const response = {
605
935
  draftId: result.id,
606
936
  bufferId: newBufferId,
607
937
  title: result.title,
@@ -616,11 +946,37 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
616
946
  hasTable: summary.hasTable,
617
947
  headings: summary.headingCount,
618
948
  },
619
- message: "Draft created. IMPORTANT: Display the URL above to the user so they can review and publish.",
620
- ...(summary.plantumlCount === 0 && {
621
- warning: "No PlantUML diagrams found in content. If diagrams were expected, use buffer_edit with plantuml parameter.",
622
- }),
623
- });
949
+ };
950
+ // Add review workflow info when editing existing page
951
+ if (originalPageId) {
952
+ response.reviewDraft = {
953
+ originalPageId: originalPageId,
954
+ originalPageVersion: originalPageVersion,
955
+ reviewLabel: reviewLabel,
956
+ wasUpdated: !!existingReviewDraft,
957
+ note: "This is a REVIEW draft linked to the original page.",
958
+ };
959
+ response.message = existingReviewDraft
960
+ ? `Review draft updated for page ${originalPageId}. Use confluence_review_publish(${result.id}) to apply changes.`
961
+ : `Review draft created for page ${originalPageId}. Use confluence_review_publish(${result.id}) to apply changes.`;
962
+ response.nextSteps = {
963
+ toPublish: `confluence_review_publish(reviewDraftId="${result.id}")`,
964
+ toDiscard: `confluence_review_discard(reviewDraftId="${result.id}")`,
965
+ toList: "confluence_review_list()",
966
+ };
967
+ }
968
+ else {
969
+ response.message = "Draft created. IMPORTANT: Display the URL above to the user so they can review and publish.";
970
+ response.afterPublish = {
971
+ note: "After user publishes, the draft ID becomes invalid.",
972
+ editAgain: `confluence_edit("${result.space?.key}/${result.title}")`,
973
+ tip: "Use confluence_edit with the same SPACE/Title or page URL to edit the published page.",
974
+ };
975
+ }
976
+ if (summary.plantumlCount === 0) {
977
+ response.warning = "No PlantUML diagrams found in content. If diagrams were expected, use buffer_edit with plantuml parameter.";
978
+ }
979
+ return formatSuccess(response);
624
980
  }
625
981
  catch (error) {
626
982
  // Try to enhance XHTML errors with location info for targeted fixes
@@ -637,14 +993,16 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
637
993
  confluence_draft_open: {
638
994
  description: `Open an existing draft page for editing. Loads content into buffer with structure (element IDs).
639
995
 
640
- Use buffer_edit with element IDs to modify content, then confluence_draft_save. User publishes via Confluence UI.`,
996
+ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to modify, then confluence_draft_save. User publishes via Confluence UI.`,
641
997
  inputSchema: z.object({
642
998
  draftId: z.coerce.string().describe("Draft page ID"),
643
999
  }),
644
1000
  handler: async (args) => {
645
1001
  try {
646
1002
  const result = await client.getDraft(args.draftId);
647
- const content = result.body?.storage?.value || "";
1003
+ const rawContent = result.body?.storage?.value || "";
1004
+ // Collapse expanded includes back to !include directives
1005
+ const content = collapseExpandedIncludesInXhtml(rawContent);
648
1006
  // Store content with element IDs for structured editing
649
1007
  const { bufferId, structure } = storeXhtmlWithStructure(content, {
650
1008
  resourceType: "confluence_page",
@@ -676,14 +1034,15 @@ Use buffer_edit with element IDs to modify content, then confluence_draft_save.
676
1034
  },
677
1035
  },
678
1036
  confluence_draft_list: {
679
- description: `List your draft pages. Use confluence_draft_open to load a draft for editing.`,
1037
+ description: `List your draft pages. Use confluence_draft_open to load a draft for editing.
1038
+
1039
+ Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
680
1040
  inputSchema: z.object({
681
1041
  spaceKey: z.string().optional().describe("Filter by space key"),
682
- limit: z.number().optional().describe("Max results (default: 25)"),
683
1042
  }),
684
1043
  handler: async (args) => {
685
1044
  try {
686
- const result = await client.listUserDrafts(args.spaceKey, args.limit);
1045
+ const result = await client.listUserDrafts(args.spaceKey);
687
1046
  // Build the base URL for constructing full URLs
688
1047
  const baseUrl = client.getBaseUrl();
689
1048
  const drafts = result.results.map((page) => ({
@@ -694,9 +1053,12 @@ Use buffer_edit with element IDs to modify content, then confluence_draft_save.
694
1053
  created: page.version?.when || "",
695
1054
  url: `${baseUrl}/pages/resumedraft.action?draftId=${page.id}`,
696
1055
  }));
697
- return formatSuccess({
1056
+ return formatSuccessBuffered({
698
1057
  drafts,
699
1058
  total: result.totalSize,
1059
+ }, {
1060
+ resourceType: "confluence_drafts",
1061
+ title: args.spaceKey ? `Drafts in ${args.spaceKey}` : "All Drafts",
700
1062
  });
701
1063
  }
702
1064
  catch (error) {
@@ -760,7 +1122,7 @@ Returns new draftId, bufferId, structure (element IDs), and URL. Always use the
760
1122
  });
761
1123
  }
762
1124
  // Validate XHTML and PlantUML content before writing
763
- const validationError = await validateContentForWrite(savedContent);
1125
+ const validationError = await validateContentForWrite(savedContent, args.bufferId);
764
1126
  if (validationError) {
765
1127
  return validationError;
766
1128
  }
@@ -859,6 +1221,195 @@ Drafts are NOT sent to trash - they are permanently deleted.`,
859
1221
  }
860
1222
  },
861
1223
  },
1224
+ // ========================================
1225
+ // Review Workflow Tools
1226
+ // ========================================
1227
+ // These tools manage the "[jicon-mcp REVIEW]" workflow for editing existing pages.
1228
+ // Review drafts are linked to original pages via labels and can be published
1229
+ // to apply changes to the original page.
1230
+ confluence_review_publish: {
1231
+ description: `Publish a review draft to apply changes to the original page.
1232
+
1233
+ This tool:
1234
+ 1. Validates the draft is a "[jicon-mcp REVIEW]" draft with proper label
1235
+ 2. Copies the draft content to the original page (creates new version)
1236
+ 3. Deletes the review draft
1237
+
1238
+ Use this after user has reviewed the draft in Confluence UI.`,
1239
+ inputSchema: z.object({
1240
+ reviewDraftId: z.coerce.string().describe("ID of the [jicon-mcp REVIEW] draft to publish"),
1241
+ }),
1242
+ handler: async (args) => {
1243
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
1244
+ const REVIEW_LABEL_PREFIX = 'jicon-review-';
1245
+ try {
1246
+ // 1. Get review draft with labels
1247
+ const reviewDraft = await client.getDraft(args.reviewDraftId, [
1248
+ ...DEFAULT_PAGE_EXPAND,
1249
+ "metadata.labels",
1250
+ ]);
1251
+ // 2. Validate it's a review draft
1252
+ if (!reviewDraft.title.startsWith(REVIEW_PREFIX)) {
1253
+ return formatError({
1254
+ error: true,
1255
+ message: `Not a jicon review draft. Title must start with "${REVIEW_PREFIX}"`,
1256
+ statusCode: 400,
1257
+ details: {
1258
+ actualTitle: reviewDraft.title,
1259
+ hint: "Use confluence_draft_create with pageId to create a review draft",
1260
+ },
1261
+ });
1262
+ }
1263
+ // 3. Find original page from label
1264
+ const labels = reviewDraft.metadata?.labels?.results || [];
1265
+ const reviewLabel = labels.find((l) => l.name.startsWith(REVIEW_LABEL_PREFIX));
1266
+ if (!reviewLabel) {
1267
+ return formatError({
1268
+ error: true,
1269
+ message: "Review draft missing link to original page (no jicon-review-* label found)",
1270
+ statusCode: 400,
1271
+ details: {
1272
+ labels: labels.map((l) => l.name),
1273
+ hint: "This draft may have been created manually. Use confluence_draft_create with pageId to create proper review drafts.",
1274
+ },
1275
+ });
1276
+ }
1277
+ const originalPageId = reviewLabel.name.replace(REVIEW_LABEL_PREFIX, '');
1278
+ // 4. Fetch original page to get current version
1279
+ let originalPage;
1280
+ try {
1281
+ originalPage = await client.getPage(originalPageId, ["version", "space"]);
1282
+ }
1283
+ catch (error) {
1284
+ return formatError({
1285
+ error: true,
1286
+ message: `Original page ${originalPageId} not found. It may have been deleted.`,
1287
+ statusCode: 404,
1288
+ details: {
1289
+ originalPageId,
1290
+ hint: "If the original page was deleted, use confluence_review_discard to remove this review draft.",
1291
+ },
1292
+ });
1293
+ }
1294
+ // 5. Get the review draft content
1295
+ const reviewContent = reviewDraft.body?.storage?.value || "";
1296
+ // 6. Update original page with review content
1297
+ const updatedPage = await client.updatePage(originalPageId, originalPage.version?.number || 1, originalPage.title, // Keep original title (not the REVIEW prefix)
1298
+ reviewContent, false // Not a minor edit
1299
+ );
1300
+ // 7. Delete review draft
1301
+ await client.deleteDraft(args.reviewDraftId);
1302
+ // 8. Invalidate buffers
1303
+ contentBuffer.invalidateByMetadata({ resourceId: originalPageId });
1304
+ contentBuffer.invalidateByMetadata({ resourceId: args.reviewDraftId });
1305
+ // Build the page URL
1306
+ const baseUrl = client.getBaseUrl();
1307
+ const pageUrl = `${baseUrl}${updatedPage._links?.webui || `/pages/viewpage.action?pageId=${originalPageId}`}`;
1308
+ return formatSuccess({
1309
+ success: true,
1310
+ originalPageId,
1311
+ originalTitle: updatedPage.title,
1312
+ newVersion: updatedPage.version?.number,
1313
+ reviewDraftDeleted: args.reviewDraftId,
1314
+ viewUrl: pageUrl,
1315
+ message: `Changes from review draft applied to page "${updatedPage.title}" (version ${updatedPage.version?.number}). Review draft deleted.`,
1316
+ });
1317
+ }
1318
+ catch (error) {
1319
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1320
+ }
1321
+ },
1322
+ },
1323
+ confluence_review_discard: {
1324
+ description: `Discard a review draft without applying changes to the original page.
1325
+
1326
+ This tool:
1327
+ 1. Validates the draft is a "[jicon-mcp REVIEW]" draft
1328
+ 2. Deletes the review draft permanently
1329
+ 3. Original page remains unchanged`,
1330
+ inputSchema: z.object({
1331
+ reviewDraftId: z.coerce.string().describe("ID of the [jicon-mcp REVIEW] draft to discard"),
1332
+ }),
1333
+ handler: async (args) => {
1334
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
1335
+ try {
1336
+ // 1. Get review draft to validate
1337
+ const reviewDraft = await client.getDraft(args.reviewDraftId);
1338
+ // 2. Validate it's a review draft
1339
+ if (!reviewDraft.title.startsWith(REVIEW_PREFIX)) {
1340
+ return formatError({
1341
+ error: true,
1342
+ message: `Not a jicon review draft. Title must start with "${REVIEW_PREFIX}"`,
1343
+ statusCode: 400,
1344
+ details: {
1345
+ actualTitle: reviewDraft.title,
1346
+ hint: "Use confluence_draft_delete for non-review drafts",
1347
+ },
1348
+ });
1349
+ }
1350
+ // 3. Delete review draft
1351
+ await client.deleteDraft(args.reviewDraftId);
1352
+ // 4. Invalidate buffer
1353
+ contentBuffer.invalidateByMetadata({ resourceId: args.reviewDraftId });
1354
+ return formatSuccess({
1355
+ success: true,
1356
+ discarded: true,
1357
+ reviewDraftId: args.reviewDraftId,
1358
+ originalTitle: reviewDraft.title.replace(REVIEW_PREFIX, ''),
1359
+ message: "Review draft discarded. Original page was not modified.",
1360
+ });
1361
+ }
1362
+ catch (error) {
1363
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1364
+ }
1365
+ },
1366
+ },
1367
+ confluence_review_list: {
1368
+ description: `List all "[jicon-mcp REVIEW]" drafts for cleanup or management.
1369
+
1370
+ Returns review drafts with their linked original page IDs.
1371
+ Use this to find abandoned review drafts or manage multiple review workflows.`,
1372
+ inputSchema: z.object({
1373
+ spaceKey: z.string().optional().describe("Filter by space key (optional)"),
1374
+ }),
1375
+ handler: async (args) => {
1376
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
1377
+ const REVIEW_LABEL_PREFIX = 'jicon-review-';
1378
+ try {
1379
+ // 1. List user's drafts with labels expanded
1380
+ const drafts = await client.listUserDrafts(args.spaceKey, 500);
1381
+ // 2. Filter to review drafts only
1382
+ const reviewDrafts = drafts.results.filter((draft) => draft.title?.startsWith(REVIEW_PREFIX));
1383
+ // 3. Build response with original page info from labels
1384
+ const baseUrl = client.getBaseUrl();
1385
+ const reviews = reviewDrafts.map((draft) => {
1386
+ const labels = draft.metadata?.labels?.results || [];
1387
+ const reviewLabel = labels.find((l) => l.name.startsWith(REVIEW_LABEL_PREFIX));
1388
+ const originalPageId = reviewLabel?.name.replace(REVIEW_LABEL_PREFIX, '') || 'unknown';
1389
+ return {
1390
+ reviewDraftId: draft.id,
1391
+ title: draft.title,
1392
+ originalTitle: draft.title.replace(REVIEW_PREFIX, ''),
1393
+ originalPageId,
1394
+ spaceKey: draft.space?.key || '',
1395
+ spaceName: draft.space?.name || '',
1396
+ createdDate: draft.version?.when || '',
1397
+ editUrl: `${baseUrl}/pages/resumedraft.action?draftId=${draft.id}`,
1398
+ };
1399
+ });
1400
+ return formatSuccess({
1401
+ reviewDrafts: reviews,
1402
+ total: reviews.length,
1403
+ message: reviews.length > 0
1404
+ ? `Found ${reviews.length} review draft(s). Use confluence_review_publish or confluence_review_discard to manage.`
1405
+ : "No review drafts found.",
1406
+ });
1407
+ }
1408
+ catch (error) {
1409
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1410
+ }
1411
+ },
1412
+ },
862
1413
  };
863
1414
  }
864
1415
  //# sourceMappingURL=tools.js.map