@crypto512/jicon-mcp 1.1.1 → 1.2.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 (68) hide show
  1. package/PROMPT.md +214 -0
  2. package/README.md +54 -8
  3. package/TOOL_LIST.md +241 -58
  4. package/dist/confluence/client.d.ts +7 -2
  5. package/dist/confluence/client.d.ts.map +1 -1
  6. package/dist/confluence/client.js +28 -10
  7. package/dist/confluence/client.js.map +1 -1
  8. package/dist/confluence/tools.d.ts +64 -8
  9. package/dist/confluence/tools.d.ts.map +1 -1
  10. package/dist/confluence/tools.js +587 -33
  11. package/dist/confluence/tools.js.map +1 -1
  12. package/dist/permissions/tool-registry.d.ts +9 -9
  13. package/dist/permissions/tool-registry.d.ts.map +1 -1
  14. package/dist/permissions/tool-registry.js +10 -3
  15. package/dist/permissions/tool-registry.js.map +1 -1
  16. package/dist/permissions/write-home-validator.d.ts.map +1 -1
  17. package/dist/permissions/write-home-validator.js +23 -3
  18. package/dist/permissions/write-home-validator.js.map +1 -1
  19. package/dist/tempo/client.js +1 -1
  20. package/dist/tempo/client.js.map +1 -1
  21. package/dist/tempo/tools.js +1 -1
  22. package/dist/tempo/tools.js.map +1 -1
  23. package/dist/utils/buffer-tools.d.ts +10 -0
  24. package/dist/utils/buffer-tools.d.ts.map +1 -1
  25. package/dist/utils/buffer-tools.js +139 -28
  26. package/dist/utils/buffer-tools.js.map +1 -1
  27. package/dist/utils/content-buffer.d.ts +5 -1
  28. package/dist/utils/content-buffer.d.ts.map +1 -1
  29. package/dist/utils/content-buffer.js +6 -3
  30. package/dist/utils/content-buffer.js.map +1 -1
  31. package/dist/utils/jicon-help.d.ts +1 -1
  32. package/dist/utils/jicon-help.d.ts.map +1 -1
  33. package/dist/utils/jicon-help.js +78 -19
  34. package/dist/utils/jicon-help.js.map +1 -1
  35. package/dist/utils/plantuml/client.d.ts +15 -1
  36. package/dist/utils/plantuml/client.d.ts.map +1 -1
  37. package/dist/utils/plantuml/client.js +56 -3
  38. package/dist/utils/plantuml/client.js.map +1 -1
  39. package/dist/utils/plantuml/include-expander.d.ts +15 -0
  40. package/dist/utils/plantuml/include-expander.d.ts.map +1 -1
  41. package/dist/utils/plantuml/include-expander.js +47 -8
  42. package/dist/utils/plantuml/include-expander.js.map +1 -1
  43. package/dist/utils/plantuml/index.d.ts +1 -1
  44. package/dist/utils/plantuml/index.d.ts.map +1 -1
  45. package/dist/utils/plantuml/index.js +1 -1
  46. package/dist/utils/plantuml/index.js.map +1 -1
  47. package/dist/utils/plantuml/service.d.ts +1 -1
  48. package/dist/utils/plantuml/service.d.ts.map +1 -1
  49. package/dist/utils/plantuml/service.js +1 -1
  50. package/dist/utils/plantuml/service.js.map +1 -1
  51. package/dist/utils/plantuml/tools.d.ts.map +1 -1
  52. package/dist/utils/plantuml/tools.js +5 -2
  53. package/dist/utils/plantuml/tools.js.map +1 -1
  54. package/dist/utils/url-tools.d.ts +27 -1
  55. package/dist/utils/url-tools.d.ts.map +1 -1
  56. package/dist/utils/url-tools.js +142 -1
  57. package/dist/utils/url-tools.js.map +1 -1
  58. package/dist/utils/xhtml/index.d.ts +1 -1
  59. package/dist/utils/xhtml/index.d.ts.map +1 -1
  60. package/dist/utils/xhtml/index.js +1 -1
  61. package/dist/utils/xhtml/index.js.map +1 -1
  62. package/dist/utils/xhtml/plantuml.d.ts +24 -6
  63. package/dist/utils/xhtml/plantuml.d.ts.map +1 -1
  64. package/dist/utils/xhtml/plantuml.js +70 -12
  65. package/dist/utils/xhtml/plantuml.js.map +1 -1
  66. package/dist/utils/xhtml/validator.js +2 -2
  67. package/dist/utils/xhtml/validator.js.map +1 -1
  68. package/package.json +2 -2
@@ -7,7 +7,9 @@ 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.
@@ -225,7 +227,7 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
225
227
  hint += "- ✅ RIGHT: (text~\"a\" OR text~\"b\") - Each term needs its own text~\n";
226
228
  }
227
229
  hint += "\nExamples:\n";
228
- hint += " text~\"Nicolas Pernoud\" - finds pages mentioning this person\n";
230
+ hint += " text~\"Mike Tasc\" - finds pages mentioning this person\n";
229
231
  hint += " text~\"meeting\" AND space=MESH - finds meetings in MESH space\n";
230
232
  hint += " title~\"sprint review\" - finds pages with sprint review in title";
231
233
  return formatError({
@@ -256,7 +258,7 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
256
258
  PREFERRED method when you have a page ID from search results. Faster and more reliable than get_page_by_title.
257
259
 
258
260
  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.`,
261
+ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.`,
260
262
  inputSchema: z.object({
261
263
  pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
262
264
  expand: z
@@ -267,7 +269,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
267
269
  handler: async (args) => {
268
270
  try {
269
271
  const result = await client.getPage(args.pageId, args.expand);
270
- const content = result.body?.storage?.value || "";
272
+ const rawContent = result.body?.storage?.value || "";
273
+ // Collapse expanded includes back to !include directives
274
+ const content = collapseExpandedIncludesInXhtml(rawContent);
271
275
  // Store content with element IDs for structured editing
272
276
  const { bufferId, structure } = storeXhtmlWithStructure(content, {
273
277
  resourceType: "confluence_page",
@@ -284,7 +288,7 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
284
288
  bufferId,
285
289
  ...(structure && { structure }),
286
290
  contentSize: content.length,
287
- message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create for user review.",
291
+ message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
288
292
  });
289
293
  }
290
294
  catch (error) {
@@ -298,7 +302,7 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
298
302
  Use ONLY when you don't have a page ID. IMPORTANT: Use the space KEY (e.g. 'MESH', 'TC'), NOT the space name.
299
303
 
300
304
  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.`,
305
+ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.`,
302
306
  inputSchema: z.object({
303
307
  spaceKey: z.string().describe("Space key (short code like 'MESH', 'TC'), NOT the full space name"),
304
308
  title: z.string().describe("Page title"),
@@ -314,7 +318,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
314
318
  statusCode: 404,
315
319
  });
316
320
  }
317
- const content = result.body?.storage?.value || "";
321
+ const rawContent = result.body?.storage?.value || "";
322
+ // Collapse expanded includes back to !include directives
323
+ const content = collapseExpandedIncludesInXhtml(rawContent);
318
324
  // Store content with element IDs for structured editing
319
325
  const { bufferId, structure } = storeXhtmlWithStructure(content, {
320
326
  resourceType: "confluence_page",
@@ -331,7 +337,196 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
331
337
  bufferId,
332
338
  ...(structure && { structure }),
333
339
  contentSize: content.length,
334
- message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create for user review.",
340
+ message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
341
+ });
342
+ }
343
+ catch (error) {
344
+ return formatError(isApiError(error) ? error : new Error(String(error)));
345
+ }
346
+ },
347
+ },
348
+ confluence_edit: {
349
+ description: `Smart page/draft loader - auto-resolves URLs, pageIds, draftIds, or SPACE/Title.
350
+
351
+ ACCEPTS ANY OF:
352
+ - Full URL: https://confluence.example.com/pages/viewpage.action?pageId=123
353
+ - Full URL: https://confluence.example.com/pages/resumedraft.action?draftId=456
354
+ - Full URL: https://confluence.example.com/display/SPACE/Page+Title
355
+ - Page ID: "123456"
356
+ - Draft ID: "draft:123456" (prefix with "draft:")
357
+ - Space/Title: "DOCS/API Guide"
358
+
359
+ SMART BEHAVIOR:
360
+ - URLs are parsed automatically to extract pageId or draftId
361
+ - Draft IDs: tries to load draft; if 404 (published), finds page by title
362
+ - Returns bufferId + structure + pageId for editing
363
+
364
+ WORKFLOW:
365
+ 1. confluence_edit(input) → bufferId, structure, pageId
366
+ 2. buffer_edit(bufferId, ...) → modify content
367
+ 3. confluence_draft_create(pageId=..., bufferId=...) → draft linked to original page
368
+ 4. User publishes via Confluence UI (updates original page)
369
+ 5. For more edits: confluence_edit(same URL or "SPACE/Title") → auto-resolves`,
370
+ inputSchema: z.object({
371
+ input: z.string().describe('URL, pageId, "draft:ID", or "SPACE/Title"'),
372
+ }),
373
+ handler: async (args) => {
374
+ const input = args.input.trim();
375
+ if (!input) {
376
+ return formatError({
377
+ error: true,
378
+ message: "Input is required. Provide a URL, pageId, draft:ID, or SPACE/Title.",
379
+ statusCode: 400,
380
+ });
381
+ }
382
+ // Helper to load page content and return formatted result
383
+ const loadPageContent = async (pageResult) => {
384
+ const rawContent = pageResult.body?.storage?.value || "";
385
+ const content = collapseExpandedIncludesInXhtml(rawContent);
386
+ const { bufferId, structure } = storeXhtmlWithStructure(content, {
387
+ resourceType: "confluence_page",
388
+ resourceId: String(pageResult.id),
389
+ contentType: "xhtml",
390
+ version: pageResult.version?.number,
391
+ spaceKey: pageResult.space?.key,
392
+ title: pageResult.title,
393
+ });
394
+ return {
395
+ pageId: pageResult.id,
396
+ spaceKey: pageResult.space?.key,
397
+ title: pageResult.title,
398
+ version: pageResult.version?.number,
399
+ bufferId,
400
+ structure,
401
+ contentSize: content.length,
402
+ message: "Content loaded. Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
403
+ };
404
+ };
405
+ // Helper to load draft content
406
+ const loadDraftContent = async (draftId) => {
407
+ const result = await client.getDraft(draftId);
408
+ const rawContent = result.body?.storage?.value || "";
409
+ const content = collapseExpandedIncludesInXhtml(rawContent);
410
+ const { bufferId, structure } = storeXhtmlWithStructure(content, {
411
+ resourceType: "confluence_page",
412
+ resourceId: String(result.id),
413
+ contentType: "xhtml",
414
+ spaceKey: result.space?.key,
415
+ title: result.title,
416
+ isDraft: true,
417
+ });
418
+ return {
419
+ draftId: result.id,
420
+ spaceKey: result.space?.key,
421
+ title: result.title,
422
+ bufferId,
423
+ structure,
424
+ contentSize: content.length,
425
+ message: "Draft loaded. Use buffer_edit to modify, then confluence_draft_save to update.",
426
+ };
427
+ };
428
+ try {
429
+ // Case 1: Full URL
430
+ if (input.startsWith("http://") || input.startsWith("https://")) {
431
+ const parsed = parseUrl(input);
432
+ if (parsed.type === "confluence_page" && parsed.pageId) {
433
+ const result = await client.getPage(parsed.pageId);
434
+ return formatSuccess(await loadPageContent(result));
435
+ }
436
+ if (parsed.type === "confluence_draft" && parsed.draftId) {
437
+ try {
438
+ return formatSuccess(await loadDraftContent(parsed.draftId));
439
+ }
440
+ catch (error) {
441
+ // Draft may have been published - try to find page by title
442
+ if (isApiError(error) && error.statusCode === 404 && parsed.spaceKey && parsed.title) {
443
+ const pageResult = await client.getPageByTitle(parsed.spaceKey, parsed.title);
444
+ if (pageResult) {
445
+ return formatSuccess({
446
+ ...await loadPageContent(pageResult),
447
+ note: "Draft was published. Loaded the published page instead.",
448
+ });
449
+ }
450
+ }
451
+ throw error;
452
+ }
453
+ }
454
+ if (parsed.type === "confluence_space_path" && parsed.spaceKey && parsed.title) {
455
+ const pageResult = await client.getPageByTitle(parsed.spaceKey, parsed.title);
456
+ if (!pageResult) {
457
+ return formatError({
458
+ error: true,
459
+ message: `Page not found: "${parsed.title}" in space ${parsed.spaceKey}`,
460
+ statusCode: 404,
461
+ });
462
+ }
463
+ return formatSuccess(await loadPageContent(pageResult));
464
+ }
465
+ return formatError({
466
+ error: true,
467
+ message: `Unable to parse URL: "${input}". Use Confluence page, draft, or display URL.`,
468
+ statusCode: 400,
469
+ });
470
+ }
471
+ // Case 2: Draft ID (prefix with "draft:")
472
+ if (input.startsWith("draft:")) {
473
+ const draftId = input.substring(6);
474
+ try {
475
+ return formatSuccess(await loadDraftContent(draftId));
476
+ }
477
+ catch (error) {
478
+ if (isApiError(error) && error.statusCode === 404) {
479
+ return formatError({
480
+ error: true,
481
+ message: `Draft ${draftId} not found. It may have been published. Try using the page URL or "SPACE/Title" format.`,
482
+ statusCode: 404,
483
+ details: {
484
+ hint: "If the draft was published, ask the user for the published page URL.",
485
+ },
486
+ });
487
+ }
488
+ throw error;
489
+ }
490
+ }
491
+ // Case 3: Space/Title format (e.g., "DOCS/API Guide")
492
+ if (input.includes("/") && !input.match(/^\d+$/)) {
493
+ const slashIndex = input.indexOf("/");
494
+ const spaceKey = input.substring(0, slashIndex).trim();
495
+ const title = input.substring(slashIndex + 1).trim();
496
+ if (!spaceKey || !title) {
497
+ return formatError({
498
+ error: true,
499
+ message: `Invalid SPACE/Title format: "${input}". Expected "SPACEKEY/Page Title".`,
500
+ statusCode: 400,
501
+ });
502
+ }
503
+ const pageResult = await client.getPageByTitle(spaceKey, title);
504
+ if (!pageResult) {
505
+ return formatError({
506
+ error: true,
507
+ message: `Page not found: "${title}" in space ${spaceKey}`,
508
+ statusCode: 404,
509
+ });
510
+ }
511
+ return formatSuccess(await loadPageContent(pageResult));
512
+ }
513
+ // Case 4: Pure numeric page ID
514
+ if (/^\d+$/.test(input)) {
515
+ const result = await client.getPage(input);
516
+ return formatSuccess(await loadPageContent(result));
517
+ }
518
+ return formatError({
519
+ error: true,
520
+ message: `Unable to parse input: "${input}". Expected URL, pageId, "draft:ID", or "SPACE/Title".`,
521
+ statusCode: 400,
522
+ details: {
523
+ examples: [
524
+ "https://confluence.example.com/pages/viewpage.action?pageId=123",
525
+ "123456 (page ID)",
526
+ "draft:456789 (draft ID)",
527
+ "DOCS/API Guide (space/title)",
528
+ ],
529
+ },
335
530
  });
336
531
  }
337
532
  catch (error) {
@@ -521,22 +716,32 @@ Returns the user's personal space key and details. Use this to verify your perso
521
716
  confluence_draft_create: {
522
717
  description: `Create a Confluence draft for user review. Returns draftId, bufferId, structure (element IDs), and clickable URL.
523
718
 
719
+ IMPORTANT: Call help(topic="plantuml") BEFORE creating content with PlantUML diagrams.
720
+ IMPORTANT: Call help(topic="storage") BEFORE creating XHTML content for proper syntax.
524
721
  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.
722
+ IMPORTANT: Raw @startuml outside macros is NOT supported. Use buffer_edit with plantuml parameter.
526
723
 
527
- TIP: See help(topic="storage") for XHTML syntax, help(topic="plantuml") for diagrams.
724
+ Two modes:
725
+ 1. NEW PAGE: Provide spaceKey + title + content/bufferId → creates standalone draft
726
+ 2. EDIT EXISTING PAGE (Review Workflow): Provide pageId + bufferId
727
+ - Creates "[jicon-mcp REVIEW] Title" draft linked to original via label
728
+ - REQUIRED: bufferId must come from confluence_get_page(pageId) or confluence_edit(pageId)
729
+ - Use confluence_review_publish(draftId) to apply changes to original
730
+ - Use confluence_review_discard(draftId) to cancel without changes
731
+ - Use confluence_review_list() to find all review drafts
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
+ Workflow for editing existing page:
734
+ 1. confluence_get_page(pageId)bufferId, structure
735
+ 2. buffer_edit(bufferId, ...) modify content
736
+ 3. confluence_draft_create(pageId=..., bufferId=...) creates review draft
737
+ 4. User reviews in Confluence UI
738
+ 5. confluence_review_publish(draftId) applies changes to original page
535
739
 
536
740
  Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit).`,
537
741
  inputSchema: z.object({
538
- spaceKey: z.string().describe("Space key"),
539
- title: z.string().describe("Page title"),
742
+ pageId: z.coerce.string().optional().describe("Existing page ID to create edit draft for. When provided, bufferId must come from that page."),
743
+ spaceKey: z.string().optional().describe("Space key (required for new pages, auto-populated when pageId is provided)"),
744
+ title: z.string().optional().describe("Page title (required for new pages, auto-populated when pageId is provided)"),
540
745
  content: z
541
746
  .string()
542
747
  .optional()
@@ -546,6 +751,83 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
546
751
  labels: z.array(z.string()).optional().describe("Array of labels"),
547
752
  }),
548
753
  handler: async (args) => {
754
+ // Determine the mode: editing existing page vs creating new draft
755
+ let spaceKey = args.spaceKey;
756
+ let title = args.title;
757
+ let parentId = args.parentId;
758
+ let originalPageId;
759
+ let originalPageVersion;
760
+ if (args.pageId) {
761
+ // MODE: Edit existing page - validate bufferId came from this page
762
+ if (!args.bufferId) {
763
+ return formatError({
764
+ error: true,
765
+ message: "When pageId is provided, bufferId is required (must come from confluence_get_page or confluence_edit of that page)",
766
+ statusCode: 400,
767
+ details: {
768
+ hint: "First call confluence_get_page(pageId) or confluence_edit(pageId) to get a bufferId, then modify with buffer_edit",
769
+ },
770
+ });
771
+ }
772
+ // Validate buffer originated from the specified page
773
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
774
+ if (!bufferInfo) {
775
+ return formatError({
776
+ error: true,
777
+ message: `Buffer not found or expired: ${args.bufferId}`,
778
+ statusCode: 404,
779
+ });
780
+ }
781
+ const bufferSourceId = bufferInfo.metadata?.resourceId;
782
+ if (bufferSourceId !== args.pageId) {
783
+ return formatError({
784
+ error: true,
785
+ message: `Buffer '${args.bufferId}' does not belong to page '${args.pageId}'`,
786
+ statusCode: 400,
787
+ details: {
788
+ bufferSource: bufferSourceId || "unknown",
789
+ expectedPage: args.pageId,
790
+ 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",
791
+ },
792
+ });
793
+ }
794
+ // Fetch original page info to get space, title, parent, etc.
795
+ try {
796
+ const originalPage = await client.getPage(args.pageId, ["space", "ancestors", "version"]);
797
+ spaceKey = originalPage.space?.key;
798
+ title = args.title || originalPage.title; // Allow title override
799
+ originalPageId = originalPage.id;
800
+ originalPageVersion = originalPage.version?.number;
801
+ // Get parent from ancestors if not explicitly provided
802
+ if (!parentId && originalPage.ancestors && originalPage.ancestors.length > 0) {
803
+ parentId = originalPage.ancestors[originalPage.ancestors.length - 1].id;
804
+ }
805
+ if (!spaceKey) {
806
+ return formatError({
807
+ error: true,
808
+ message: `Could not determine space key for page ${args.pageId}`,
809
+ statusCode: 400,
810
+ });
811
+ }
812
+ }
813
+ catch (error) {
814
+ return formatError({
815
+ error: true,
816
+ message: `Failed to fetch original page ${args.pageId}: ${error instanceof Error ? error.message : String(error)}`,
817
+ statusCode: 404,
818
+ });
819
+ }
820
+ }
821
+ else {
822
+ // MODE: New page - require spaceKey and title
823
+ if (!spaceKey || !title) {
824
+ return formatError({
825
+ error: true,
826
+ message: "For new drafts, both 'spaceKey' and 'title' are required. For editing existing pages, provide 'pageId' instead.",
827
+ statusCode: 400,
828
+ });
829
+ }
830
+ }
549
831
  // Resolve content from either content string or bufferId
550
832
  const resolved = resolveContentFromBuffer(args.content, args.bufferId);
551
833
  if (resolved.error) {
@@ -580,14 +862,66 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
580
862
  // Expand PlantUML !include directives before sending to Confluence
581
863
  const expandedContent = await expandPlantUmlInXhtml(content);
582
864
  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
- });
865
+ // Constants for review workflow
866
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
867
+ const REVIEW_LABEL_PREFIX = 'jicon-review-';
868
+ let result;
869
+ let existingReviewDraft = null;
870
+ let reviewLabel;
871
+ if (originalPageId) {
872
+ // REVIEW WORKFLOW: Creating/updating review draft for existing page
873
+ reviewLabel = `${REVIEW_LABEL_PREFIX}${originalPageId}`;
874
+ // Check if review draft already exists for this page
875
+ existingReviewDraft = await client.findDraftByLabel(reviewLabel);
876
+ if (existingReviewDraft) {
877
+ // Delete existing review draft to update it (API doesn't support draft updates)
878
+ await client.deleteDraft(existingReviewDraft.id);
879
+ }
880
+ // Prevent nested review drafts
881
+ const reviewTitle = title.startsWith(REVIEW_PREFIX)
882
+ ? title
883
+ : `${REVIEW_PREFIX}${title}`;
884
+ // Create review draft with label linking to original page
885
+ const labels = [...(args.labels || []), reviewLabel];
886
+ result = await client.createDraft({
887
+ spaceKey: spaceKey,
888
+ title: reviewTitle,
889
+ content: expandedContent,
890
+ parentId: parentId,
891
+ labels: labels,
892
+ });
893
+ }
894
+ else {
895
+ // NEW PAGE MODE: Create standalone draft
896
+ result = await client.createDraft({
897
+ spaceKey: spaceKey,
898
+ title: title,
899
+ content: expandedContent,
900
+ parentId: parentId,
901
+ labels: args.labels,
902
+ });
903
+ }
904
+ // Verify draft is readable with labels (catches label attachment failures)
905
+ if (originalPageId && reviewLabel) {
906
+ const verifyDraft = await client.getDraft(result.id, [
907
+ ...DEFAULT_PAGE_EXPAND,
908
+ "metadata.labels",
909
+ ]);
910
+ const labels = verifyDraft.metadata?.labels?.results || [];
911
+ const hasReviewLabel = labels.some((l) => l.name === reviewLabel);
912
+ if (!hasReviewLabel) {
913
+ // Label attachment may have failed - try to add it again
914
+ // This can happen due to eventual consistency or API timing
915
+ try {
916
+ await client.addLabels(result.id, [reviewLabel], true);
917
+ }
918
+ catch {
919
+ // If label add fails, warn but continue (draft still exists)
920
+ }
921
+ }
922
+ }
590
923
  // Store content with element IDs for structured editing
924
+ // Track originalPageId in metadata so we can link the draft to its source
591
925
  const { bufferId: newBufferId } = storeXhtmlWithStructure(content, {
592
926
  resourceType: "confluence_page",
593
927
  resourceId: result.id,
@@ -595,13 +929,16 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
595
929
  spaceKey: result.space?.key,
596
930
  title: result.title,
597
931
  isDraft: true,
932
+ originalPageId: originalPageId,
933
+ originalPageVersion: originalPageVersion,
598
934
  });
599
935
  // Build the draft edit URL (drafts use resumedraft.action, not the webui link)
600
936
  const baseUrl = client.getBaseUrl();
601
937
  const draftUrl = `${baseUrl}/pages/resumedraft.action?draftId=${result.id}`;
602
938
  // Generate content summary for verification
603
939
  const summary = getContentSummary(content);
604
- return formatSuccess({
940
+ // Build response with original page info if editing existing page
941
+ const response = {
605
942
  draftId: result.id,
606
943
  bufferId: newBufferId,
607
944
  title: result.title,
@@ -616,11 +953,37 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
616
953
  hasTable: summary.hasTable,
617
954
  headings: summary.headingCount,
618
955
  },
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
- });
956
+ };
957
+ // Add review workflow info when editing existing page
958
+ if (originalPageId) {
959
+ response.reviewDraft = {
960
+ originalPageId: originalPageId,
961
+ originalPageVersion: originalPageVersion,
962
+ reviewLabel: reviewLabel,
963
+ wasUpdated: !!existingReviewDraft,
964
+ note: "This is a REVIEW draft linked to the original page.",
965
+ };
966
+ response.message = existingReviewDraft
967
+ ? `Review draft updated for page ${originalPageId}. Use confluence_review_publish(${result.id}) to apply changes.`
968
+ : `Review draft created for page ${originalPageId}. Use confluence_review_publish(${result.id}) to apply changes.`;
969
+ response.nextSteps = {
970
+ toPublish: `confluence_review_publish(reviewDraftId="${result.id}")`,
971
+ toDiscard: `confluence_review_discard(reviewDraftId="${result.id}")`,
972
+ toList: "confluence_review_list()",
973
+ };
974
+ }
975
+ else {
976
+ response.message = "Draft created. IMPORTANT: Display the URL above to the user so they can review and publish.";
977
+ response.afterPublish = {
978
+ note: "After user publishes, the draft ID becomes invalid.",
979
+ editAgain: `confluence_edit("${result.space?.key}/${result.title}")`,
980
+ tip: "Use confluence_edit with the same SPACE/Title or page URL to edit the published page.",
981
+ };
982
+ }
983
+ if (summary.plantumlCount === 0) {
984
+ response.warning = "No PlantUML diagrams found in content. If diagrams were expected, use buffer_edit with plantuml parameter.";
985
+ }
986
+ return formatSuccess(response);
624
987
  }
625
988
  catch (error) {
626
989
  // Try to enhance XHTML errors with location info for targeted fixes
@@ -637,14 +1000,16 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
637
1000
  confluence_draft_open: {
638
1001
  description: `Open an existing draft page for editing. Loads content into buffer with structure (element IDs).
639
1002
 
640
- Use buffer_edit with element IDs to modify content, then confluence_draft_save. User publishes via Confluence UI.`,
1003
+ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to modify, then confluence_draft_save. User publishes via Confluence UI.`,
641
1004
  inputSchema: z.object({
642
1005
  draftId: z.coerce.string().describe("Draft page ID"),
643
1006
  }),
644
1007
  handler: async (args) => {
645
1008
  try {
646
1009
  const result = await client.getDraft(args.draftId);
647
- const content = result.body?.storage?.value || "";
1010
+ const rawContent = result.body?.storage?.value || "";
1011
+ // Collapse expanded includes back to !include directives
1012
+ const content = collapseExpandedIncludesInXhtml(rawContent);
648
1013
  // Store content with element IDs for structured editing
649
1014
  const { bufferId, structure } = storeXhtmlWithStructure(content, {
650
1015
  resourceType: "confluence_page",
@@ -859,6 +1224,195 @@ Drafts are NOT sent to trash - they are permanently deleted.`,
859
1224
  }
860
1225
  },
861
1226
  },
1227
+ // ========================================
1228
+ // Review Workflow Tools
1229
+ // ========================================
1230
+ // These tools manage the "[jicon-mcp REVIEW]" workflow for editing existing pages.
1231
+ // Review drafts are linked to original pages via labels and can be published
1232
+ // to apply changes to the original page.
1233
+ confluence_review_publish: {
1234
+ description: `Publish a review draft to apply changes to the original page.
1235
+
1236
+ This tool:
1237
+ 1. Validates the draft is a "[jicon-mcp REVIEW]" draft with proper label
1238
+ 2. Copies the draft content to the original page (creates new version)
1239
+ 3. Deletes the review draft
1240
+
1241
+ Use this after user has reviewed the draft in Confluence UI.`,
1242
+ inputSchema: z.object({
1243
+ reviewDraftId: z.coerce.string().describe("ID of the [jicon-mcp REVIEW] draft to publish"),
1244
+ }),
1245
+ handler: async (args) => {
1246
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
1247
+ const REVIEW_LABEL_PREFIX = 'jicon-review-';
1248
+ try {
1249
+ // 1. Get review draft with labels
1250
+ const reviewDraft = await client.getDraft(args.reviewDraftId, [
1251
+ ...DEFAULT_PAGE_EXPAND,
1252
+ "metadata.labels",
1253
+ ]);
1254
+ // 2. Validate it's a review draft
1255
+ if (!reviewDraft.title.startsWith(REVIEW_PREFIX)) {
1256
+ return formatError({
1257
+ error: true,
1258
+ message: `Not a jicon review draft. Title must start with "${REVIEW_PREFIX}"`,
1259
+ statusCode: 400,
1260
+ details: {
1261
+ actualTitle: reviewDraft.title,
1262
+ hint: "Use confluence_draft_create with pageId to create a review draft",
1263
+ },
1264
+ });
1265
+ }
1266
+ // 3. Find original page from label
1267
+ const labels = reviewDraft.metadata?.labels?.results || [];
1268
+ const reviewLabel = labels.find((l) => l.name.startsWith(REVIEW_LABEL_PREFIX));
1269
+ if (!reviewLabel) {
1270
+ return formatError({
1271
+ error: true,
1272
+ message: "Review draft missing link to original page (no jicon-review-* label found)",
1273
+ statusCode: 400,
1274
+ details: {
1275
+ labels: labels.map((l) => l.name),
1276
+ hint: "This draft may have been created manually. Use confluence_draft_create with pageId to create proper review drafts.",
1277
+ },
1278
+ });
1279
+ }
1280
+ const originalPageId = reviewLabel.name.replace(REVIEW_LABEL_PREFIX, '');
1281
+ // 4. Fetch original page to get current version
1282
+ let originalPage;
1283
+ try {
1284
+ originalPage = await client.getPage(originalPageId, ["version", "space"]);
1285
+ }
1286
+ catch (error) {
1287
+ return formatError({
1288
+ error: true,
1289
+ message: `Original page ${originalPageId} not found. It may have been deleted.`,
1290
+ statusCode: 404,
1291
+ details: {
1292
+ originalPageId,
1293
+ hint: "If the original page was deleted, use confluence_review_discard to remove this review draft.",
1294
+ },
1295
+ });
1296
+ }
1297
+ // 5. Get the review draft content
1298
+ const reviewContent = reviewDraft.body?.storage?.value || "";
1299
+ // 6. Update original page with review content
1300
+ const updatedPage = await client.updatePage(originalPageId, originalPage.version?.number || 1, originalPage.title, // Keep original title (not the REVIEW prefix)
1301
+ reviewContent, false // Not a minor edit
1302
+ );
1303
+ // 7. Delete review draft
1304
+ await client.deleteDraft(args.reviewDraftId);
1305
+ // 8. Invalidate buffers
1306
+ contentBuffer.invalidateByMetadata({ resourceId: originalPageId });
1307
+ contentBuffer.invalidateByMetadata({ resourceId: args.reviewDraftId });
1308
+ // Build the page URL
1309
+ const baseUrl = client.getBaseUrl();
1310
+ const pageUrl = `${baseUrl}${updatedPage._links?.webui || `/pages/viewpage.action?pageId=${originalPageId}`}`;
1311
+ return formatSuccess({
1312
+ success: true,
1313
+ originalPageId,
1314
+ originalTitle: updatedPage.title,
1315
+ newVersion: updatedPage.version?.number,
1316
+ reviewDraftDeleted: args.reviewDraftId,
1317
+ viewUrl: pageUrl,
1318
+ message: `Changes from review draft applied to page "${updatedPage.title}" (version ${updatedPage.version?.number}). Review draft deleted.`,
1319
+ });
1320
+ }
1321
+ catch (error) {
1322
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1323
+ }
1324
+ },
1325
+ },
1326
+ confluence_review_discard: {
1327
+ description: `Discard a review draft without applying changes to the original page.
1328
+
1329
+ This tool:
1330
+ 1. Validates the draft is a "[jicon-mcp REVIEW]" draft
1331
+ 2. Deletes the review draft permanently
1332
+ 3. Original page remains unchanged`,
1333
+ inputSchema: z.object({
1334
+ reviewDraftId: z.coerce.string().describe("ID of the [jicon-mcp REVIEW] draft to discard"),
1335
+ }),
1336
+ handler: async (args) => {
1337
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
1338
+ try {
1339
+ // 1. Get review draft to validate
1340
+ const reviewDraft = await client.getDraft(args.reviewDraftId);
1341
+ // 2. Validate it's a review draft
1342
+ if (!reviewDraft.title.startsWith(REVIEW_PREFIX)) {
1343
+ return formatError({
1344
+ error: true,
1345
+ message: `Not a jicon review draft. Title must start with "${REVIEW_PREFIX}"`,
1346
+ statusCode: 400,
1347
+ details: {
1348
+ actualTitle: reviewDraft.title,
1349
+ hint: "Use confluence_draft_delete for non-review drafts",
1350
+ },
1351
+ });
1352
+ }
1353
+ // 3. Delete review draft
1354
+ await client.deleteDraft(args.reviewDraftId);
1355
+ // 4. Invalidate buffer
1356
+ contentBuffer.invalidateByMetadata({ resourceId: args.reviewDraftId });
1357
+ return formatSuccess({
1358
+ success: true,
1359
+ discarded: true,
1360
+ reviewDraftId: args.reviewDraftId,
1361
+ originalTitle: reviewDraft.title.replace(REVIEW_PREFIX, ''),
1362
+ message: "Review draft discarded. Original page was not modified.",
1363
+ });
1364
+ }
1365
+ catch (error) {
1366
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1367
+ }
1368
+ },
1369
+ },
1370
+ confluence_review_list: {
1371
+ description: `List all "[jicon-mcp REVIEW]" drafts for cleanup or management.
1372
+
1373
+ Returns review drafts with their linked original page IDs.
1374
+ Use this to find abandoned review drafts or manage multiple review workflows.`,
1375
+ inputSchema: z.object({
1376
+ spaceKey: z.string().optional().describe("Filter by space key (optional)"),
1377
+ }),
1378
+ handler: async (args) => {
1379
+ const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
1380
+ const REVIEW_LABEL_PREFIX = 'jicon-review-';
1381
+ try {
1382
+ // 1. List user's drafts with labels expanded
1383
+ const drafts = await client.listUserDrafts(args.spaceKey, 500);
1384
+ // 2. Filter to review drafts only
1385
+ const reviewDrafts = drafts.results.filter((draft) => draft.title?.startsWith(REVIEW_PREFIX));
1386
+ // 3. Build response with original page info from labels
1387
+ const baseUrl = client.getBaseUrl();
1388
+ const reviews = reviewDrafts.map((draft) => {
1389
+ const labels = draft.metadata?.labels?.results || [];
1390
+ const reviewLabel = labels.find((l) => l.name.startsWith(REVIEW_LABEL_PREFIX));
1391
+ const originalPageId = reviewLabel?.name.replace(REVIEW_LABEL_PREFIX, '') || 'unknown';
1392
+ return {
1393
+ reviewDraftId: draft.id,
1394
+ title: draft.title,
1395
+ originalTitle: draft.title.replace(REVIEW_PREFIX, ''),
1396
+ originalPageId,
1397
+ spaceKey: draft.space?.key || '',
1398
+ spaceName: draft.space?.name || '',
1399
+ createdDate: draft.version?.when || '',
1400
+ editUrl: `${baseUrl}/pages/resumedraft.action?draftId=${draft.id}`,
1401
+ };
1402
+ });
1403
+ return formatSuccess({
1404
+ reviewDrafts: reviews,
1405
+ total: reviews.length,
1406
+ message: reviews.length > 0
1407
+ ? `Found ${reviews.length} review draft(s). Use confluence_review_publish or confluence_review_discard to manage.`
1408
+ : "No review drafts found.",
1409
+ });
1410
+ }
1411
+ catch (error) {
1412
+ return formatError(isApiError(error) ? error : new Error(String(error)));
1413
+ }
1414
+ },
1415
+ },
862
1416
  };
863
1417
  }
864
1418
  //# sourceMappingURL=tools.js.map