@crypto512/jicon-mcp 1.2.0 → 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.
@@ -2,7 +2,7 @@
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";
@@ -46,72 +46,17 @@ function getContentSummary(content) {
46
46
  result.headingCount = doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
47
47
  return result;
48
48
  }
49
- /**
50
- * Resolve content from either direct content string or bufferId.
51
- * Returns { content, error } - if error is set, return it as the tool result.
52
- */
53
- function resolveContentFromBuffer(contentArg, bufferIdArg) {
54
- // Exactly one of content or bufferId must be provided
55
- if (contentArg && bufferIdArg) {
56
- return {
57
- error: formatError({
58
- error: true,
59
- message: "Provide either 'content' or 'bufferId', not both",
60
- statusCode: 400,
61
- }),
62
- };
63
- }
64
- if (bufferIdArg) {
65
- const bufferChunk = contentBuffer.getChunk(bufferIdArg);
66
- if (!bufferChunk) {
67
- return {
68
- error: formatError({
69
- error: true,
70
- message: `Buffer ${bufferIdArg} not found or expired`,
71
- statusCode: 404,
72
- }),
73
- };
74
- }
75
- // Validate that buffer contains XHTML content for Confluence
76
- const bufferInfo = contentBuffer.getInfo(bufferIdArg);
77
- if (bufferInfo?.metadata?.contentType && bufferInfo.metadata.contentType !== "xhtml") {
78
- return {
79
- error: formatError({
80
- error: true,
81
- message: `Buffer ${bufferIdArg} contains ${bufferInfo.metadata.contentType} content, but Confluence requires XHTML.`,
82
- statusCode: 400,
83
- details: {
84
- hint: "Create Confluence content with buffer_create, then edit with buffer_edit.",
85
- foundContentType: bufferInfo.metadata.contentType,
86
- expectedContentType: "xhtml",
87
- },
88
- }),
89
- };
90
- }
91
- // Get full content from buffer
92
- const fullContent = contentBuffer.getChunk(bufferIdArg, 0, bufferChunk.totalSize);
93
- if (!fullContent) {
94
- return {
95
- error: formatError({
96
- error: true,
97
- message: "Failed to retrieve full buffer content",
98
- statusCode: 500,
99
- }),
100
- };
101
- }
102
- return { content: fullContent.chunk };
103
- }
104
- // Direct content provided
105
- return { content: contentArg };
106
- }
107
49
  /**
108
50
  * Validate XHTML content before Confluence write operations.
109
51
  * Returns error result if validation fails, null if valid.
52
+ * When bufferId is provided, includes it in error response for recovery.
110
53
  */
111
- async function validateContentForWrite(content) {
54
+ async function validateContentForWrite(content, bufferId) {
112
55
  const validation = await validateXhtmlAsync(content, { validatePlantUml: true });
113
56
  if (!validation.valid) {
114
57
  const errorMessages = [];
58
+ let errorElementId;
59
+ let errorContext;
115
60
  // XHTML structure errors
116
61
  if (validation.errors && validation.errors.length > 0) {
117
62
  errorMessages.push("XHTML validation errors:");
@@ -124,6 +69,14 @@ async function validateContentForWrite(content) {
124
69
  ? err.location.context.substring(0, 60) + "..."
125
70
  : err.location.context;
126
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;
127
80
  }
128
81
  });
129
82
  }
@@ -151,10 +104,18 @@ async function validateContentForWrite(content) {
151
104
  errorMessages.push(" </ac:structured-macro>");
152
105
  }
153
106
  }
154
- // Add help hints
107
+ // Add recovery instructions
155
108
  errorMessages.push("");
156
- errorMessages.push('TIP: Call help(topic="storage") for Confluence XHTML format guide.');
157
- 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).');
158
119
  errorMessages.push("");
159
120
  errorMessages.push("ACTION REQUIRED: Fix content errors before calling this tool again.");
160
121
  errorMessages.push("DO NOT claim success - the draft was NOT created.");
@@ -164,7 +125,12 @@ async function validateContentForWrite(content) {
164
125
  statusCode: 400,
165
126
  details: {
166
127
  validationErrors: errorMessages,
167
- 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`,
168
134
  },
169
135
  });
170
136
  }
@@ -185,7 +151,9 @@ function storeXhtmlWithStructure(content, metadata) {
185
151
  export function createConfluenceTools(client) {
186
152
  return {
187
153
  confluence_search_content: {
188
- 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.
189
157
 
190
158
  TIP: See help(topic="cql") for CQL syntax guide.
191
159
 
@@ -199,8 +167,10 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
199
167
  handler: async (args) => {
200
168
  try {
201
169
  const result = await client.searchContentAll(args.cql, args.expand);
202
- // Use formatSuccess for automatic buffering of large responses
203
- return formatSuccess(result);
170
+ return formatSuccessBuffered(result, {
171
+ resourceType: "confluence_search",
172
+ title: `CQL: ${args.cql.substring(0, 100)}${args.cql.length > 100 ? "..." : ""}`,
173
+ });
204
174
  }
205
175
  catch (error) {
206
176
  // Enhanced error handling for common CQL errors
@@ -561,7 +531,9 @@ WORKFLOW:
561
531
  },
562
532
  },
563
533
  confluence_list_spaces: {
564
- 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.`,
565
537
  inputSchema: z.object({
566
538
  type: z
567
539
  .enum(["global", "personal"])
@@ -571,7 +543,10 @@ WORKFLOW:
571
543
  handler: async (args) => {
572
544
  try {
573
545
  const result = await client.listSpaces(args.type);
574
- return formatSuccess(result);
546
+ return formatSuccessBuffered(result, {
547
+ resourceType: "confluence_spaces",
548
+ title: args.type ? `${args.type} spaces` : "All Spaces",
549
+ });
575
550
  }
576
551
  catch (error) {
577
552
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -579,7 +554,9 @@ WORKFLOW:
579
554
  },
580
555
  },
581
556
  confluence_get_space: {
582
- 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.`,
583
560
  inputSchema: z.object({
584
561
  spaceKey: z.string().describe("Space key"),
585
562
  expand: z.array(z.string()).optional().describe("Additional data to expand"),
@@ -587,8 +564,11 @@ WORKFLOW:
587
564
  handler: async (args) => {
588
565
  try {
589
566
  const result = await client.getSpace(args.spaceKey, args.expand);
590
- // Use formatSuccess for automatic buffering of large responses
591
- return formatSuccess(result);
567
+ return formatSuccessBuffered(result, {
568
+ resourceType: "confluence_space",
569
+ title: result.name || args.spaceKey,
570
+ spaceKey: args.spaceKey,
571
+ });
592
572
  }
593
573
  catch (error) {
594
574
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -596,7 +576,9 @@ WORKFLOW:
596
576
  },
597
577
  },
598
578
  confluence_get_page_children: {
599
- 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.`,
600
582
  inputSchema: z.object({
601
583
  pageId: z.coerce.string().describe("Parent page ID (accepts string or number)"),
602
584
  expand: z.array(z.string()).optional().describe("Additional data to expand"),
@@ -604,7 +586,11 @@ WORKFLOW:
604
586
  handler: async (args) => {
605
587
  try {
606
588
  const result = await client.getPageChildren(args.pageId, args.expand);
607
- return formatSuccess(result);
589
+ return formatSuccessBuffered(result, {
590
+ resourceType: "confluence_page_children",
591
+ title: `Page ${args.pageId} children`,
592
+ pageId: args.pageId,
593
+ });
608
594
  }
609
595
  catch (error) {
610
596
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -628,14 +614,20 @@ WORKFLOW:
628
614
  },
629
615
  },
630
616
  confluence_get_comments: {
631
- 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.`,
632
620
  inputSchema: z.object({
633
621
  pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
634
622
  }),
635
623
  handler: async (args) => {
636
624
  try {
637
625
  const result = await client.getComments(args.pageId);
638
- return formatSuccess(result);
626
+ return formatSuccessBuffered(result, {
627
+ resourceType: "confluence_comments",
628
+ title: `Page ${args.pageId} comments`,
629
+ pageId: args.pageId,
630
+ });
639
631
  }
640
632
  catch (error) {
641
633
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -660,14 +652,20 @@ WORKFLOW:
660
652
  },
661
653
  },
662
654
  confluence_list_attachments: {
663
- 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.`,
664
658
  inputSchema: z.object({
665
659
  pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
666
660
  }),
667
661
  handler: async (args) => {
668
662
  try {
669
663
  const result = await client.listAttachments(args.pageId);
670
- return formatSuccess(result);
664
+ return formatSuccessBuffered(result, {
665
+ resourceType: "confluence_attachments",
666
+ title: `Page ${args.pageId} attachments`,
667
+ pageId: args.pageId,
668
+ });
671
669
  }
672
670
  catch (error) {
673
671
  return formatError(isApiError(error) ? error : new Error(String(error)));
@@ -716,37 +714,31 @@ Returns the user's personal space key and details. Use this to verify your perso
716
714
  confluence_draft_create: {
717
715
  description: `Create a Confluence draft for user review. Returns draftId, bufferId, structure (element IDs), and clickable URL.
718
716
 
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.
721
- IMPORTANT: User must validate the draft in Confluence UI before publishing.
722
- IMPORTANT: Raw @startuml outside macros is NOT supported. Use buffer_edit with plantuml parameter.
717
+ REQUIRES bufferId - content must be in a buffer for validation and error recovery.
723
718
 
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
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
732
724
 
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
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
739
732
 
740
- Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit).`,
733
+ On validation error: returns bufferId + errorElementId for surgical fix with buffer_edit.
734
+
735
+ IMPORTANT: Call help(topic="storage") for XHTML syntax (HTML vs XHTML differences).
736
+ IMPORTANT: Call help(topic="plantuml") for diagram syntax.`,
741
737
  inputSchema: z.object({
742
738
  pageId: z.coerce.string().optional().describe("Existing page ID to create edit draft for. When provided, bufferId must come from that page."),
743
739
  spaceKey: z.string().optional().describe("Space key (required for new pages, auto-populated when pageId is provided)"),
744
740
  title: z.string().optional().describe("Page title (required for new pages, auto-populated when pageId is provided)"),
745
- content: z
746
- .string()
747
- .optional()
748
- .describe("Page content in Confluence storage format (XHTML-based)"),
749
- bufferId: z.string().optional().describe("Buffer ID containing content (alternative to content)"),
741
+ bufferId: z.string().describe("Buffer ID containing XHTML content (from buffer_create or confluence_get_page)"),
750
742
  parentId: z.string().optional().describe("Parent page ID"),
751
743
  labels: z.array(z.string()).optional().describe("Array of labels"),
752
744
  }),
@@ -757,27 +749,41 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
757
749
  let parentId = args.parentId;
758
750
  let originalPageId;
759
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
+ });
763
+ }
764
+ // Validate buffer contains XHTML content
765
+ if (bufferInfo.metadata?.contentType && bufferInfo.metadata.contentType !== "xhtml") {
766
+ return formatError({
767
+ error: true,
768
+ message: `Buffer '${args.bufferId}' is not XHTML content (found: ${bufferInfo.metadata.contentType})`,
769
+ statusCode: 400,
770
+ details: {
771
+ hint: "Use buffer_create(content='...', contentType='xhtml') to create an XHTML buffer",
772
+ },
773
+ });
774
+ }
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;
760
785
  if (args.pageId) {
761
786
  // 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
787
  const bufferSourceId = bufferInfo.metadata?.resourceId;
782
788
  if (bufferSourceId !== args.pageId) {
783
789
  return formatError({
@@ -828,19 +834,6 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
828
834
  });
829
835
  }
830
836
  }
831
- // Resolve content from either content string or bufferId
832
- const resolved = resolveContentFromBuffer(args.content, args.bufferId);
833
- if (resolved.error) {
834
- return resolved.error;
835
- }
836
- if (!resolved.content) {
837
- return formatError({
838
- error: true,
839
- message: "Either 'content' or 'bufferId' must be provided",
840
- statusCode: 400,
841
- });
842
- }
843
- const content = resolved.content;
844
837
  // Check for raw PlantUML that should use buffer_edit with plantuml parameter
845
838
  const rawPlantUml = detectRawPlantUml(content);
846
839
  if (rawPlantUml) {
@@ -855,7 +848,7 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
855
848
  });
856
849
  }
857
850
  // Validate XHTML and PlantUML content before writing
858
- const validationError = await validateContentForWrite(content);
851
+ const validationError = await validateContentForWrite(content, args.bufferId);
859
852
  if (validationError) {
860
853
  return validationError;
861
854
  }
@@ -1041,14 +1034,15 @@ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to modify, th
1041
1034
  },
1042
1035
  },
1043
1036
  confluence_draft_list: {
1044
- 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.`,
1045
1040
  inputSchema: z.object({
1046
1041
  spaceKey: z.string().optional().describe("Filter by space key"),
1047
- limit: z.number().optional().describe("Max results (default: 25)"),
1048
1042
  }),
1049
1043
  handler: async (args) => {
1050
1044
  try {
1051
- const result = await client.listUserDrafts(args.spaceKey, args.limit);
1045
+ const result = await client.listUserDrafts(args.spaceKey);
1052
1046
  // Build the base URL for constructing full URLs
1053
1047
  const baseUrl = client.getBaseUrl();
1054
1048
  const drafts = result.results.map((page) => ({
@@ -1059,9 +1053,12 @@ Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to modify, th
1059
1053
  created: page.version?.when || "",
1060
1054
  url: `${baseUrl}/pages/resumedraft.action?draftId=${page.id}`,
1061
1055
  }));
1062
- return formatSuccess({
1056
+ return formatSuccessBuffered({
1063
1057
  drafts,
1064
1058
  total: result.totalSize,
1059
+ }, {
1060
+ resourceType: "confluence_drafts",
1061
+ title: args.spaceKey ? `Drafts in ${args.spaceKey}` : "All Drafts",
1065
1062
  });
1066
1063
  }
1067
1064
  catch (error) {
@@ -1125,7 +1122,7 @@ Returns new draftId, bufferId, structure (element IDs), and URL. Always use the
1125
1122
  });
1126
1123
  }
1127
1124
  // Validate XHTML and PlantUML content before writing
1128
- const validationError = await validateContentForWrite(savedContent);
1125
+ const validationError = await validateContentForWrite(savedContent, args.bufferId);
1129
1126
  if (validationError) {
1130
1127
  return validationError;
1131
1128
  }