@crypto512/jicon-mcp 1.0.3 → 1.1.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 (116) hide show
  1. package/README.md +40 -3
  2. package/TOOL_LIST.md +261 -140
  3. package/dist/config/constants.d.ts +2 -0
  4. package/dist/config/constants.d.ts.map +1 -1
  5. package/dist/config/constants.js +2 -0
  6. package/dist/config/constants.js.map +1 -1
  7. package/dist/config/loader.d.ts.map +1 -1
  8. package/dist/config/loader.js +2 -0
  9. package/dist/config/loader.js.map +1 -1
  10. package/dist/config/types.d.ts +6 -0
  11. package/dist/config/types.d.ts.map +1 -1
  12. package/dist/config/types.js +4 -0
  13. package/dist/config/types.js.map +1 -1
  14. package/dist/confluence/client.d.ts +11 -2
  15. package/dist/confluence/client.d.ts.map +1 -1
  16. package/dist/confluence/client.js +29 -12
  17. package/dist/confluence/client.js.map +1 -1
  18. package/dist/confluence/tools.d.ts +2 -2
  19. package/dist/confluence/tools.d.ts.map +1 -1
  20. package/dist/confluence/tools.js +145 -59
  21. package/dist/confluence/tools.js.map +1 -1
  22. package/dist/index.js +17 -2
  23. package/dist/index.js.map +1 -1
  24. package/dist/jira/client.d.ts.map +1 -1
  25. package/dist/jira/client.js +3 -13
  26. package/dist/jira/client.js.map +1 -1
  27. package/dist/jira/tools.d.ts.map +1 -1
  28. package/dist/jira/tools.js +9 -30
  29. package/dist/jira/tools.js.map +1 -1
  30. package/dist/permissions/tool-registry.d.ts +4 -2
  31. package/dist/permissions/tool-registry.d.ts.map +1 -1
  32. package/dist/permissions/tool-registry.js +14 -2
  33. package/dist/permissions/tool-registry.js.map +1 -1
  34. package/dist/tempo/formatters.d.ts.map +1 -1
  35. package/dist/tempo/formatters.js +1 -16
  36. package/dist/tempo/formatters.js.map +1 -1
  37. package/dist/tempo/tools.d.ts +2 -2
  38. package/dist/tempo/tools.d.ts.map +1 -1
  39. package/dist/tempo/tools.js +5 -13
  40. package/dist/tempo/tools.js.map +1 -1
  41. package/dist/utils/buffer-tools.d.ts +115 -63
  42. package/dist/utils/buffer-tools.d.ts.map +1 -1
  43. package/dist/utils/buffer-tools.js +385 -330
  44. package/dist/utils/buffer-tools.js.map +1 -1
  45. package/dist/utils/content-buffer.d.ts +24 -1
  46. package/dist/utils/content-buffer.d.ts.map +1 -1
  47. package/dist/utils/content-buffer.js +49 -39
  48. package/dist/utils/content-buffer.js.map +1 -1
  49. package/dist/utils/jicon-help.d.ts +5 -5
  50. package/dist/utils/jicon-help.d.ts.map +1 -1
  51. package/dist/utils/jicon-help.js +259 -536
  52. package/dist/utils/jicon-help.js.map +1 -1
  53. package/dist/utils/plantuml/client.d.ts +1 -1
  54. package/dist/utils/plantuml/client.d.ts.map +1 -1
  55. package/dist/utils/plantuml/client.js +34 -59
  56. package/dist/utils/plantuml/client.js.map +1 -1
  57. package/dist/utils/plantuml/docker-manager.d.ts +3 -1
  58. package/dist/utils/plantuml/docker-manager.d.ts.map +1 -1
  59. package/dist/utils/plantuml/docker-manager.js +6 -2
  60. package/dist/utils/plantuml/docker-manager.js.map +1 -1
  61. package/dist/utils/plantuml/include-expander.d.ts +32 -0
  62. package/dist/utils/plantuml/include-expander.d.ts.map +1 -0
  63. package/dist/utils/plantuml/include-expander.js +129 -0
  64. package/dist/utils/plantuml/include-expander.js.map +1 -0
  65. package/dist/utils/plantuml/index.d.ts +2 -1
  66. package/dist/utils/plantuml/index.d.ts.map +1 -1
  67. package/dist/utils/plantuml/index.js +3 -1
  68. package/dist/utils/plantuml/index.js.map +1 -1
  69. package/dist/utils/plantuml/service.d.ts +32 -8
  70. package/dist/utils/plantuml/service.d.ts.map +1 -1
  71. package/dist/utils/plantuml/service.js +108 -15
  72. package/dist/utils/plantuml/service.js.map +1 -1
  73. package/dist/utils/plantuml/tools.d.ts +4 -4
  74. package/dist/utils/plantuml/tools.d.ts.map +1 -1
  75. package/dist/utils/plantuml/tools.js +50 -5
  76. package/dist/utils/plantuml/tools.js.map +1 -1
  77. package/dist/utils/plantuml/types.d.ts +4 -4
  78. package/dist/utils/plantuml/validation-helper.d.ts +33 -0
  79. package/dist/utils/plantuml/validation-helper.d.ts.map +1 -0
  80. package/dist/utils/plantuml/validation-helper.js +97 -0
  81. package/dist/utils/plantuml/validation-helper.js.map +1 -0
  82. package/dist/utils/response-formatter.js +7 -7
  83. package/dist/utils/response-formatter.js.map +1 -1
  84. package/dist/utils/time-formatter.d.ts +10 -0
  85. package/dist/utils/time-formatter.d.ts.map +1 -0
  86. package/dist/utils/time-formatter.js +22 -0
  87. package/dist/utils/time-formatter.js.map +1 -0
  88. package/dist/utils/url-tools.d.ts.map +1 -1
  89. package/dist/utils/url-tools.js +11 -4
  90. package/dist/utils/url-tools.js.map +1 -1
  91. package/dist/utils/xhtml/error-locator.d.ts +70 -0
  92. package/dist/utils/xhtml/error-locator.d.ts.map +1 -0
  93. package/dist/utils/xhtml/error-locator.js +229 -0
  94. package/dist/utils/xhtml/error-locator.js.map +1 -0
  95. package/dist/utils/xhtml/index.d.ts +6 -3
  96. package/dist/utils/xhtml/index.d.ts.map +1 -1
  97. package/dist/utils/xhtml/index.js +7 -3
  98. package/dist/utils/xhtml/index.js.map +1 -1
  99. package/dist/utils/xhtml/operations.d.ts +54 -0
  100. package/dist/utils/xhtml/operations.d.ts.map +1 -1
  101. package/dist/utils/xhtml/operations.js +205 -0
  102. package/dist/utils/xhtml/operations.js.map +1 -1
  103. package/dist/utils/xhtml/plantuml.d.ts +1 -1
  104. package/dist/utils/xhtml/plantuml.d.ts.map +1 -1
  105. package/dist/utils/xhtml/plantuml.js +19 -30
  106. package/dist/utils/xhtml/plantuml.js.map +1 -1
  107. package/dist/utils/xhtml/serializer.d.ts +5 -0
  108. package/dist/utils/xhtml/serializer.d.ts.map +1 -1
  109. package/dist/utils/xhtml/serializer.js +18 -4
  110. package/dist/utils/xhtml/serializer.js.map +1 -1
  111. package/dist/utils/xhtml/types.d.ts +3 -3
  112. package/dist/utils/xhtml/types.d.ts.map +1 -1
  113. package/dist/utils/xhtml/validator.d.ts.map +1 -1
  114. package/dist/utils/xhtml/validator.js +87 -4
  115. package/dist/utils/xhtml/validator.js.map +1 -1
  116. package/package.json +1 -1
@@ -10,8 +10,10 @@ import * as fs from "fs";
10
10
  import * as path from "path";
11
11
  import { contentBuffer } from "./content-buffer.js";
12
12
  import { formatSuccess, formatError, getMaxOutputSize } from "./response-formatter.js";
13
- import { BufferEditXhtmlSchema, parseXhtml, serializeXhtml, executeOperation, validateXhtml, validateXhtmlAsync, validatePlantUml, buildPlantUmlMacro, querySelector, updatePlantUmlInMacro, resolveSemanticPosition, findUnsupportedPseudo, } from "./xhtml/index.js";
14
- import { validate as validatePlantUmlWithDocker, isAvailable as isPlantUmlServiceAvailable } from "./plantuml/service.js";
13
+ import { parseXhtml, serializeXhtml, validateXhtmlAsync, buildPlantUmlMacro, detectRawPlantUml,
14
+ // Element ID-based operations
15
+ parseStructure, insertById, appendToDocument, replaceById, removeById, } from "./xhtml/index.js";
16
+ import { validatePlantUmlWithFallback } from "./plantuml/index.js";
15
17
  /**
16
18
  * Format grep results as text output similar to grep CLI
17
19
  */
@@ -86,9 +88,14 @@ function isBinaryContentType(contentType) {
86
88
  export function createBufferTools() {
87
89
  return {
88
90
  buffer_create: {
89
- description: `Create a new buffer with initial content. Use this to draft new content (e.g., for new Confluence pages) before persisting. Returns bufferId for use with buffer_edit and other buffer tools.`,
91
+ description: `Create a new buffer with initial content. Returns bufferId and structure (for XHTML).
92
+
93
+ For XHTML content, returns structure with element IDs for use with buffer_edit:
94
+ - Each element gets a unique ID (stable during session)
95
+ - Use buffer_edit with after/before/replace to modify by element ID`,
90
96
  inputSchema: z.object({
91
97
  content: z.string().describe("Initial content for the buffer"),
98
+ contentType: z.enum(["xhtml", "plain", "json"]).describe("Content type: 'xhtml' for Confluence, 'plain' for text, 'json' for data"),
92
99
  metadata: z
93
100
  .record(z.unknown())
94
101
  .optional()
@@ -96,15 +103,65 @@ export function createBufferTools() {
96
103
  }),
97
104
  handler: async (args) => {
98
105
  try {
99
- const bufferId = contentBuffer.store(args.content, args.metadata);
106
+ const mergedMetadata = {
107
+ ...args.metadata,
108
+ contentType: args.contentType,
109
+ };
110
+ let bufferId;
111
+ let structure;
112
+ let nextId;
113
+ if (args.contentType === "xhtml") {
114
+ // Check for common CDATA mistake: wrapping entire content in CDATA
115
+ const trimmedContent = args.content.trim();
116
+ if (trimmedContent.startsWith("<![CDATA[")) {
117
+ return formatError({
118
+ error: true,
119
+ message: "Content starts with CDATA wrapper. Remove <![CDATA[ and ]]> from the content parameter.",
120
+ statusCode: 400,
121
+ details: {
122
+ hint: "CDATA is only used inside <ac:plain-text-body> elements (for code blocks and PlantUML). Never wrap the entire content in CDATA.",
123
+ example: "Use: <h1>Title</h1><p>Text</p>\nNot: <![CDATA[<h1>Title</h1><p>Text</p>]]>",
124
+ helpTopic: 'Call help(topic="storage") for Confluence XHTML format guide.',
125
+ },
126
+ });
127
+ }
128
+ // Parse XHTML and build structure with element IDs
129
+ const parseResult = parseXhtml(args.content);
130
+ if (!parseResult.document) {
131
+ return formatError({
132
+ error: true,
133
+ message: `Failed to parse XHTML: ${parseResult.error?.message}`,
134
+ statusCode: 400,
135
+ details: {
136
+ parseError: parseResult.error?.message,
137
+ line: parseResult.error?.line,
138
+ column: parseResult.error?.column,
139
+ },
140
+ });
141
+ }
142
+ // Assign IDs and get structure
143
+ const structureResult = parseStructure(parseResult.document);
144
+ structure = structureResult.structure;
145
+ nextId = structureResult.nextId;
146
+ // Serialize back (now with data-jicon-id attributes)
147
+ const contentWithIds = serializeXhtml(parseResult.document);
148
+ bufferId = contentBuffer.storeWithStructure(contentWithIds, structure, nextId, mergedMetadata);
149
+ }
150
+ else {
151
+ bufferId = contentBuffer.store(args.content, mergedMetadata);
152
+ }
100
153
  const info = contentBuffer.getInfo(bufferId);
101
154
  return formatSuccess({
102
155
  bufferId,
103
156
  totalSize: info?.totalSize ?? args.content.length,
157
+ contentType: args.contentType,
104
158
  createdAt: info ? new Date(info.createdAt).toISOString() : new Date().toISOString(),
105
159
  expiresAt: info ? new Date(info.expiresAt).toISOString() : undefined,
106
- metadata: args.metadata,
107
- message: "Buffer created. Use buffer_edit to modify, buffer_get_chunk to read.",
160
+ metadata: mergedMetadata,
161
+ ...(structure && { structure, nextId }),
162
+ message: args.contentType === "xhtml"
163
+ ? "Buffer created with element IDs. Use buffer_edit with after/before/replace to modify."
164
+ : "Buffer created.",
108
165
  });
109
166
  }
110
167
  catch (error) {
@@ -343,81 +400,57 @@ Example: Find buffer for draft 12345 → look for metadata.resourceId === "12345
343
400
  },
344
401
  },
345
402
  buffer_edit: {
346
- description: `Exact string replacement in buffered content. Fails if old_string is not unique (use replace_all=true for all occurrences). Changes are in-memory only - call update API to persist.
403
+ description: `Edit buffer content. For XHTML buffers, use element IDs. For plain/json, use string replacement.
347
404
 
348
- For Confluence content: Use buffer_edit_xhtml instead for structure-aware editing (tables, macros, layouts). Call help(topic="storage") for guide.`,
349
- inputSchema: z.object({
350
- bufferId: z.string().describe("Buffer ID to modify"),
351
- old_string: z.string().describe("Exact text to replace"),
352
- new_string: z.string().describe("Replacement text"),
353
- replace_all: z
354
- .boolean()
355
- .optional()
356
- .default(false)
357
- .describe("Replace all occurrences (default: false, fails if not unique)"),
358
- }),
359
- handler: async (args) => {
360
- try {
361
- // Check if buffer contains XHTML content - reject to force use of buffer_edit_xhtml
362
- const bufferInfo = contentBuffer.getInfo(args.bufferId);
363
- if (bufferInfo?.metadata?.contentType === "xhtml") {
364
- return formatError({
365
- error: true,
366
- message: "This buffer contains Confluence XHTML content. Use buffer_edit_xhtml instead for proper structure-aware editing.",
367
- statusCode: 400,
368
- details: {
369
- hint: "For PlantUML diagrams, use: buffer_edit_xhtml(bufferId, operation='insert-plantuml', ...)",
370
- contentType: "xhtml",
371
- },
372
- });
373
- }
374
- const result = contentBuffer.edit(args.bufferId, args.old_string, args.new_string, args.replace_all ?? false);
375
- if (!result) {
376
- return formatError({
377
- error: true,
378
- message: `Buffer not found or expired: ${args.bufferId}`,
379
- statusCode: 404,
380
- });
381
- }
382
- // Check if it's an error result
383
- if ("error" in result && result.error) {
384
- return formatError({
385
- error: true,
386
- message: result.message,
387
- statusCode: 400,
388
- details: { occurrences: result.occurrences },
389
- });
390
- }
391
- return formatSuccess(result);
392
- }
393
- catch (error) {
394
- return formatError(error instanceof Error ? error : new Error(String(error)));
395
- }
396
- },
397
- },
398
- buffer_edit_xhtml: {
399
- description: `Structure-aware XHTML editing for Confluence storage format. Supports CSS-like selectors to target elements, with operations: insert, insert-plantuml, update, update-plantuml, remove, move, wrap.
405
+ XHTML editing (by element ID) - single operation:
406
+ - after: Insert content after element with this ID
407
+ - before: Insert content before element with this ID
408
+ - replace: Replace element with this ID
409
+ - append: Add content at document end
410
+ - remove: Remove element with this ID
411
+ - content: XHTML to insert/replace
412
+ - plantuml: PlantUML code (auto-wrapped in Confluence macro)
400
413
 
401
- Selector syntax:
402
- - Tag: 'p', 'h1', 'table'
403
- - Namespaced: 'ac:structured-macro', 'ri:page'
404
- - Attributes: '[ac:name="plantuml"]', 'ac:structured-macro[ac:name="code"]'
405
- - Pseudo: ':nth-child(2)', ':first-child', ':last-child'
406
- - Descendants: 'table tbody tr'
407
- - Direct child: 'ul > li'
414
+ XHTML batch editing - multiple operations in one call:
415
+ - operations: Array of {after?, before?, replace?, append?, remove?, content?, plantuml?}
416
+ - Operations are executed sequentially; stops on first error
417
+ - Much more efficient than multiple tool calls (parse once, serialize once)
408
418
 
409
- Semantic positions (for insert-plantuml, alternative to selector):
410
- - 'after-title': After first h1 or h2
411
- - 'after-heading': After first heading (h1-h6)
412
- - 'before-content': At document start
413
- - 'end': At document end
414
- - 'after-toc': After table of contents macro
419
+ Example batch: operations=[{after:6, plantuml:"@startuml..."}, {after:8, plantuml:"@startuml..."}]
415
420
 
416
- PlantUML: Use plantuml_validate first for full Docker-based validation. This tool uses basic sync validation.`,
417
- inputSchema: BufferEditXhtmlSchema,
421
+ Plain/JSON editing (string replacement):
422
+ - old_string: Text to replace
423
+ - new_string: Replacement text
424
+ - replace_all: Replace all occurrences
425
+
426
+ Returns updated structure for XHTML buffers.`,
427
+ inputSchema: z.object({
428
+ bufferId: z.string().describe("Buffer ID to modify"),
429
+ // XHTML batch operations
430
+ operations: z.array(z.object({
431
+ after: z.number().optional().describe("Insert after element with this ID"),
432
+ before: z.number().optional().describe("Insert before element with this ID"),
433
+ replace: z.number().optional().describe("Replace element with this ID"),
434
+ append: z.boolean().optional().describe("Append content at document end"),
435
+ remove: z.number().optional().describe("Remove element with this ID"),
436
+ content: z.string().optional().describe("XHTML content to insert/replace"),
437
+ plantuml: z.string().optional().describe("PlantUML code (auto-wrapped in macro)"),
438
+ })).optional().describe("Array of XHTML operations to execute sequentially (more efficient than multiple calls)"),
439
+ // XHTML element ID operations (single operation - backwards compatible)
440
+ after: z.number().optional().describe("Insert after element with this ID"),
441
+ before: z.number().optional().describe("Insert before element with this ID"),
442
+ replace: z.number().optional().describe("Replace element with this ID"),
443
+ append: z.boolean().optional().describe("Append content at document end"),
444
+ remove: z.number().optional().describe("Remove element with this ID"),
445
+ content: z.string().optional().describe("XHTML content to insert/replace"),
446
+ plantuml: z.string().optional().describe("PlantUML code (auto-wrapped in macro)"),
447
+ // Plain text operations
448
+ old_string: z.string().optional().describe("Text to replace (plain/json only)"),
449
+ new_string: z.string().optional().describe("Replacement text (plain/json only)"),
450
+ replace_all: z.boolean().optional().default(false).describe("Replace all occurrences"),
451
+ }),
418
452
  handler: async (args) => {
419
453
  try {
420
- // Get buffer content
421
454
  const bufferInfo = contentBuffer.getInfo(args.bufferId);
422
455
  if (!bufferInfo) {
423
456
  return formatError({
@@ -426,326 +459,334 @@ PlantUML: Use plantuml_validate first for full Docker-based validation. This too
426
459
  statusCode: 404,
427
460
  });
428
461
  }
429
- const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
430
- if (!chunk) {
431
- return formatError({
432
- error: true,
433
- message: `Failed to read buffer: ${args.bufferId}`,
434
- statusCode: 500,
435
- });
436
- }
437
- const originalContent = chunk.chunk;
438
- const oldSize = originalContent.length;
439
- // Parse XHTML
440
- const parseResult = parseXhtml(originalContent);
441
- if (!parseResult.document) {
442
- return formatError({
443
- error: true,
444
- message: `Failed to parse XHTML: ${parseResult.error?.message}`,
445
- statusCode: 400,
446
- details: {
447
- parseError: parseResult.error?.message,
448
- line: parseResult.error?.line,
449
- column: parseResult.error?.column,
450
- context: parseResult.error?.context,
451
- hint: parseResult.error?.context
452
- ? `Check near: "${parseResult.error.context.substring(0, 60)}${parseResult.error.context.length > 60 ? "..." : ""}"`
453
- : undefined,
454
- },
455
- });
456
- }
457
- // Handle PlantUML operations specially
458
- let contentToInsert = args.content;
459
- const operation = args.operation;
460
- let resolvedSelector = args.selector;
461
- let resolvedPosition = args.position;
462
- let insertDiagramType; // Track diagram type for insert-plantuml
463
- // Check if we need to derive selector from semanticPosition
464
- if (!resolvedSelector && args.semanticPosition) {
465
- const resolved = resolveSemanticPosition(parseResult.document, args.semanticPosition);
466
- if (resolved) {
467
- resolvedSelector = resolved.selector;
468
- resolvedPosition = resolved.position;
462
+ const isXhtml = bufferInfo.metadata?.contentType === "xhtml";
463
+ // XHTML editing with element IDs
464
+ if (isXhtml) {
465
+ let operations;
466
+ if (args.operations && args.operations.length > 0) {
467
+ // Batch mode: use operations array
468
+ operations = args.operations;
469
469
  }
470
470
  else {
471
+ // Single operation mode (backwards compatible)
472
+ const hasIdOperation = args.after !== undefined || args.before !== undefined ||
473
+ args.replace !== undefined || args.append || args.remove !== undefined;
474
+ if (!hasIdOperation) {
475
+ return formatError({
476
+ error: true,
477
+ message: "XHTML buffer requires element ID operation: after, before, replace, append, remove, or operations array",
478
+ statusCode: 400,
479
+ details: {
480
+ structure: bufferInfo.structure,
481
+ nextId: bufferInfo.nextId,
482
+ hint: "Use after=ID, before=ID, replace=ID, append=true, remove=ID, or operations=[...]",
483
+ },
484
+ });
485
+ }
486
+ operations = [{
487
+ after: args.after,
488
+ before: args.before,
489
+ replace: args.replace,
490
+ append: args.append,
491
+ remove: args.remove,
492
+ content: args.content,
493
+ plantuml: args.plantuml,
494
+ }];
495
+ }
496
+ // Get buffer content
497
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
498
+ if (!chunk) {
471
499
  return formatError({
472
500
  error: true,
473
- message: `Could not resolve semantic position: ${args.semanticPosition}`,
474
- statusCode: 400,
475
- details: {
476
- semanticPosition: args.semanticPosition,
477
- hint: "Document may be empty or missing required elements",
478
- },
501
+ message: `Failed to read buffer: ${args.bufferId}`,
502
+ statusCode: 500,
479
503
  });
480
504
  }
481
- }
482
- // Check that we have a selector (either provided or derived from semanticPosition)
483
- if (!resolvedSelector) {
484
- return formatError({
485
- error: true,
486
- message: "Either selector or semanticPosition is required",
487
- statusCode: 400,
488
- details: {
489
- hint: "Provide selector (e.g., 'h1', 'table', 'ac:structured-macro[ac:name=\"plantuml\"]') or semanticPosition (e.g., 'after-title', 'end')",
490
- },
491
- });
492
- }
493
- if (operation === "insert-plantuml") {
494
- if (!args.plantuml) {
505
+ // Parse XHTML once
506
+ const parseResult = parseXhtml(chunk.chunk);
507
+ if (!parseResult.document) {
495
508
  return formatError({
496
509
  error: true,
497
- message: "plantuml parameter is required for insert-plantuml operation",
510
+ message: `Failed to parse XHTML: ${parseResult.error?.message}`,
498
511
  statusCode: 400,
499
512
  });
500
513
  }
501
- // Validate PlantUML - use Docker service if available, else fallback to basic validation
502
- let normalizedCode;
503
- if (isPlantUmlServiceAvailable()) {
504
- // Use Docker-based validation
505
- try {
506
- const dockerValidation = await validatePlantUmlWithDocker(args.plantuml);
507
- if (!dockerValidation.valid) {
514
+ const oldSize = chunk.chunk.length;
515
+ let nextId = bufferInfo.nextId ?? 1;
516
+ const allInsertedIds = [];
517
+ const diagramTypes = [];
518
+ // Execute all operations sequentially
519
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
520
+ const op = operations[opIndex];
521
+ // Build content to insert (handle plantuml specially)
522
+ let contentToInsert = op.content;
523
+ let diagramType;
524
+ if (op.plantuml) {
525
+ // Validate PlantUML
526
+ const validation = await validatePlantUmlWithFallback(op.plantuml, "insert-plantuml", {});
527
+ if (!validation.valid) {
528
+ // Return error with operation index for debugging
529
+ const errorResult = validation.error;
530
+ const errorData = JSON.parse(errorResult.content[0].text);
531
+ errorData.operationIndex = opIndex;
532
+ errorData.completedOperations = opIndex;
533
+ return {
534
+ content: [{ type: "text", text: JSON.stringify(errorData, null, 2) }],
535
+ };
536
+ }
537
+ contentToInsert = buildPlantUmlMacro(validation.normalizedCode);
538
+ diagramType = validation.diagramType;
539
+ if (diagramType)
540
+ diagramTypes.push(diagramType);
541
+ }
542
+ // Execute single operation
543
+ let result;
544
+ if (op.remove !== undefined) {
545
+ result = removeById(parseResult.document, op.remove);
546
+ }
547
+ else if (op.replace !== undefined) {
548
+ if (!contentToInsert) {
508
549
  return formatError({
509
550
  error: true,
510
- message: "PlantUML syntax error",
551
+ message: `Operation ${opIndex + 1}: content or plantuml required for replace operation`,
511
552
  statusCode: 400,
512
- details: {
513
- plantumlErrors: dockerValidation.errors,
514
- diagramType: dockerValidation.diagramType,
515
- hint: "Check plantuml_validate for detailed error info",
516
- },
517
553
  });
518
554
  }
519
- normalizedCode = dockerValidation.normalizedCode;
520
- insertDiagramType = dockerValidation.diagramType;
555
+ result = replaceById(parseResult.document, op.replace, contentToInsert);
521
556
  }
522
- catch (error) {
523
- // Docker error - fallback to basic validation
524
- const basicValidation = validatePlantUml(args.plantuml);
525
- if (!basicValidation.valid) {
557
+ else if (op.append) {
558
+ if (!contentToInsert) {
526
559
  return formatError({
527
560
  error: true,
528
- message: "PlantUML syntax error",
561
+ message: `Operation ${opIndex + 1}: content or plantuml required for append operation`,
529
562
  statusCode: 400,
530
- details: {
531
- plantumlError: basicValidation.error,
532
- },
533
563
  });
534
564
  }
535
- normalizedCode = basicValidation.normalizedCode;
536
- insertDiagramType = basicValidation.diagramType;
537
- }
538
- }
539
- else {
540
- // Use basic synchronous validation
541
- const basicValidation = validatePlantUml(args.plantuml);
542
- if (!basicValidation.valid) {
543
- return formatError({
544
- error: true,
545
- message: "PlantUML syntax error",
546
- statusCode: 400,
547
- details: {
548
- plantumlError: basicValidation.error,
549
- hint: "Use plantuml_validate for full Docker-based validation",
550
- },
551
- });
565
+ result = appendToDocument(parseResult.document, contentToInsert, nextId);
566
+ if (result.nextId)
567
+ nextId = result.nextId;
552
568
  }
553
- normalizedCode = basicValidation.normalizedCode;
554
- insertDiagramType = basicValidation.diagramType;
555
- }
556
- // Build the macro
557
- contentToInsert = buildPlantUmlMacro(normalizedCode, args.macroId);
558
- }
559
- else if (operation === "update-plantuml") {
560
- if (!args.plantuml) {
561
- return formatError({
562
- error: true,
563
- message: "plantuml parameter is required for update-plantuml operation",
564
- statusCode: 400,
565
- });
566
- }
567
- // Validate PlantUML - use Docker service if available, else fallback to basic validation
568
- let normalizedCode;
569
- let updateDiagramType;
570
- if (isPlantUmlServiceAvailable()) {
571
- // Use Docker-based validation
572
- try {
573
- const dockerValidation = await validatePlantUmlWithDocker(args.plantuml);
574
- if (!dockerValidation.valid) {
569
+ else if (op.after !== undefined) {
570
+ if (!contentToInsert) {
575
571
  return formatError({
576
572
  error: true,
577
- message: "PlantUML syntax error",
573
+ message: `Operation ${opIndex + 1}: content or plantuml required for after operation`,
578
574
  statusCode: 400,
579
- details: {
580
- plantumlErrors: dockerValidation.errors,
581
- diagramType: dockerValidation.diagramType,
582
- hint: "Check plantuml_validate for detailed error info",
583
- },
584
575
  });
585
576
  }
586
- normalizedCode = dockerValidation.normalizedCode;
587
- updateDiagramType = dockerValidation.diagramType;
577
+ result = insertById(parseResult.document, op.after, "after", contentToInsert, nextId);
578
+ if (result.nextId)
579
+ nextId = result.nextId;
588
580
  }
589
- catch (error) {
590
- // Docker error - fallback to basic validation
591
- const basicValidation = validatePlantUml(args.plantuml);
592
- if (!basicValidation.valid) {
581
+ else if (op.before !== undefined) {
582
+ if (!contentToInsert) {
593
583
  return formatError({
594
584
  error: true,
595
- message: "PlantUML syntax error",
585
+ message: `Operation ${opIndex + 1}: content or plantuml required for before operation`,
596
586
  statusCode: 400,
597
- details: {
598
- plantumlError: basicValidation.error,
599
- },
600
587
  });
601
588
  }
602
- normalizedCode = basicValidation.normalizedCode;
603
- updateDiagramType = basicValidation.diagramType;
589
+ result = insertById(parseResult.document, op.before, "before", contentToInsert, nextId);
590
+ if (result.nextId)
591
+ nextId = result.nextId;
604
592
  }
605
- }
606
- else {
607
- // Use basic synchronous validation
608
- const basicValidation = validatePlantUml(args.plantuml);
609
- if (!basicValidation.valid) {
593
+ else {
610
594
  return formatError({
611
595
  error: true,
612
- message: "PlantUML syntax error",
596
+ message: `Operation ${opIndex + 1}: No valid operation specified (need after, before, replace, append, or remove)`,
613
597
  statusCode: 400,
614
- details: {
615
- plantumlError: basicValidation.error,
616
- hint: "Use plantuml_validate for full Docker-based validation",
617
- },
618
598
  });
619
599
  }
620
- normalizedCode = basicValidation.normalizedCode;
621
- updateDiagramType = basicValidation.diagramType;
622
- }
623
- // Find the macro element and update it directly
624
- const selectorResult = querySelector(parseResult.document, args.selector);
625
- if (selectorResult.matches.length === 0) {
626
- const unsupportedPseudo = findUnsupportedPseudo(args.selector);
627
- const hint = unsupportedPseudo
628
- ? `Pseudo-selector ':${unsupportedPseudo}' is not supported. Use ':contains("text")' to find elements by text content, or use 'ac:structured-macro[ac:name="plantuml"]' with matchIndex.`
629
- : "Use 'ac:structured-macro[ac:name=\"plantuml\"]' to find PlantUML macros, or use matchIndex if multiple exist.";
630
- return formatError({
631
- error: true,
632
- message: `No elements match selector: ${args.selector}`,
633
- statusCode: 400,
634
- details: { selector: args.selector, matchCount: 0, hint },
635
- });
636
- }
637
- const targetIndex = args.matchIndex ?? 0;
638
- if (targetIndex >= selectorResult.matches.length) {
639
- return formatError({
640
- error: true,
641
- message: `matchIndex ${targetIndex} out of range (${selectorResult.matches.length} matches)`,
642
- statusCode: 400,
643
- });
644
- }
645
- const macroElement = selectorResult.matches[targetIndex].element;
646
- // Verify it's a plantuml macro
647
- if (macroElement.tagName.toLowerCase() !== "ac:structured-macro" ||
648
- macroElement.getAttribute("ac:name") !== "plantuml") {
649
- return formatError({
650
- error: true,
651
- message: "Selected element is not a PlantUML macro",
652
- statusCode: 400,
653
- });
654
- }
655
- // Update the macro content
656
- const updated = updatePlantUmlInMacro(macroElement, normalizedCode);
657
- if (!updated) {
658
- return formatError({
659
- error: true,
660
- message: "Failed to update PlantUML macro content",
661
- statusCode: 500,
662
- });
663
- }
664
- // Serialize and update buffer
665
- const newContent = serializeXhtml(parseResult.document);
666
- // Validate if requested
667
- if (args.validate !== false) {
668
- const validationResult = validateXhtml(newContent);
669
- if (!validationResult.valid) {
670
- const firstError = validationResult.errors[0];
600
+ if (!result.success) {
671
601
  return formatError({
672
602
  error: true,
673
- message: `Resulting XHTML is invalid: ${firstError?.message || "Unknown error"}`,
603
+ message: `Operation ${opIndex + 1} failed: ${result.error || "Unknown error"}`,
674
604
  statusCode: 400,
675
605
  details: {
676
- validationErrors: validationResult.errors,
677
- hint: firstError?.location?.context
678
- ? `Check near: "${firstError.location.context.substring(0, 60)}${firstError.location.context.length > 60 ? "..." : ""}"`
679
- : undefined,
606
+ operationIndex: opIndex,
607
+ completedOperations: opIndex,
608
+ operation: op,
609
+ structure: bufferInfo.structure,
610
+ nextId: bufferInfo.nextId,
680
611
  },
681
612
  });
682
613
  }
614
+ // Collect inserted IDs
615
+ if (result.insertedIds) {
616
+ allInsertedIds.push(...result.insertedIds);
617
+ }
683
618
  }
684
- // Update buffer in place (keeps same buffer ID)
685
- contentBuffer.update(args.bufferId, newContent, { contentType: "xhtml" });
619
+ // Serialize and update buffer once (after all operations)
620
+ const newContent = serializeXhtml(parseResult.document);
621
+ // Rebuild structure
622
+ const newStructure = parseStructure(parseResult.document);
623
+ // Update buffer
624
+ contentBuffer.update(args.bufferId, newContent, { contentType: "xhtml" }, newStructure.structure, newStructure.nextId);
686
625
  return formatSuccess({
687
626
  bufferId: args.bufferId,
688
627
  success: true,
689
- operation: "update-plantuml",
690
- matchCount: 1,
628
+ operationsCompleted: operations.length,
691
629
  oldSize,
692
630
  newSize: newContent.length,
693
- affectedElements: ["ac:structured-macro"],
694
- diagramType: updateDiagramType,
631
+ structure: newStructure.structure,
632
+ nextId: newStructure.nextId,
633
+ ...(allInsertedIds.length > 0 && { insertedIds: allInsertedIds }),
634
+ ...(diagramTypes.length > 0 && { diagramTypes }),
635
+ });
636
+ }
637
+ // Plain text/JSON editing
638
+ if (args.old_string === undefined || args.new_string === undefined) {
639
+ return formatError({
640
+ error: true,
641
+ message: "old_string and new_string required for plain/json buffer editing",
642
+ statusCode: 400,
643
+ });
644
+ }
645
+ const result = contentBuffer.edit(args.bufferId, args.old_string, args.new_string, args.replace_all ?? false);
646
+ if (!result) {
647
+ return formatError({
648
+ error: true,
649
+ message: `Buffer not found or expired: ${args.bufferId}`,
650
+ statusCode: 404,
695
651
  });
696
652
  }
697
- // Execute the operation
698
- const result = executeOperation(parseResult.document, operation, resolvedSelector, {
699
- position: resolvedPosition,
700
- content: contentToInsert,
701
- attributes: args.attributes,
702
- targetSelector: args.targetSelector,
703
- matchIndex: args.matchIndex,
704
- matchAll: args.matchAll,
705
- });
706
653
  if ("error" in result && result.error) {
707
654
  return formatError({
708
655
  error: true,
709
656
  message: result.message,
710
657
  statusCode: 400,
711
- details: result.details,
658
+ details: { occurrences: result.occurrences },
712
659
  });
713
660
  }
714
- // Serialize back to XHTML
715
- const newContent = serializeXhtml(parseResult.document);
716
- // Validate if requested
717
- if (args.validate !== false) {
718
- const validationResult = validateXhtml(newContent);
719
- if (!validationResult.valid) {
720
- const firstError = validationResult.errors[0];
721
- return formatError({
722
- error: true,
723
- message: `Resulting XHTML is invalid: ${firstError?.message || "Unknown error"}`,
724
- statusCode: 400,
725
- details: {
726
- validationErrors: validationResult.errors,
727
- hint: firstError?.location?.context
728
- ? `Check near: "${firstError.location.context.substring(0, 60)}${firstError.location.context.length > 60 ? "..." : ""}"`
729
- : undefined,
730
- },
731
- });
732
- }
661
+ return formatSuccess(result);
662
+ }
663
+ catch (error) {
664
+ return formatError(error instanceof Error ? error : new Error(String(error)));
665
+ }
666
+ },
667
+ },
668
+ buffer_get_structure: {
669
+ description: `Get current element structure for an XHTML buffer. Returns element IDs for use with buffer_edit.
670
+
671
+ Each element has:
672
+ - id: Unique ID (stable during session)
673
+ - type: Element type ("h1", "h2", "p", "plantuml", "ul", etc.)
674
+ - text: Preview text (truncated)
675
+ - children: Child count for lists/tables`,
676
+ inputSchema: z.object({
677
+ bufferId: z.string().describe("Buffer ID to get structure for"),
678
+ }),
679
+ handler: async (args) => {
680
+ try {
681
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
682
+ if (!bufferInfo) {
683
+ return formatError({
684
+ error: true,
685
+ message: `Buffer not found or expired: ${args.bufferId}`,
686
+ statusCode: 404,
687
+ });
688
+ }
689
+ if (bufferInfo.metadata?.contentType !== "xhtml") {
690
+ return formatError({
691
+ error: true,
692
+ message: "Buffer is not XHTML content",
693
+ statusCode: 400,
694
+ details: {
695
+ contentType: bufferInfo.metadata?.contentType,
696
+ hint: "Structure is only available for XHTML buffers",
697
+ },
698
+ });
733
699
  }
734
- // Update buffer in place (keeps same buffer ID)
735
- contentBuffer.update(args.bufferId, newContent, { contentType: "xhtml" });
736
- // Check if buffer is associated with a Confluence draft and add nextStep hint
737
- const updatedBufferInfo = contentBuffer.getInfo(args.bufferId);
738
- const isDraft = updatedBufferInfo?.metadata?.isDraft === true;
739
- const isConfluencePage = updatedBufferInfo?.metadata?.resourceType === "confluence_page";
740
700
  return formatSuccess({
741
- ...result,
742
701
  bufferId: args.bufferId,
743
- oldSize,
744
- newSize: newContent.length,
745
- ...(insertDiagramType && { diagramType: insertDiagramType }),
746
- ...((isDraft || isConfluencePage) && {
747
- nextStep: "Call confluence_draft_save(bufferId) to persist changes to Confluence",
748
- }),
702
+ structure: bufferInfo.structure ?? [],
703
+ nextId: bufferInfo.nextId ?? 1,
704
+ });
705
+ }
706
+ catch (error) {
707
+ return formatError(error instanceof Error ? error : new Error(String(error)));
708
+ }
709
+ },
710
+ },
711
+ buffer_get_element: {
712
+ description: `Get the raw XHTML content of a specific element by ID.
713
+
714
+ Use this to inspect problematic elements when fixing XHTML parsing errors.
715
+ Returns the element's outerHTML, type, and line count.
716
+
717
+ Example: After a Confluence error at element 12, use this to see the content:
718
+ buffer_get_element(bufferId="buf_xxx", elementId=12)`,
719
+ inputSchema: z.object({
720
+ bufferId: z.string().describe("Buffer ID containing XHTML content"),
721
+ elementId: z.number().describe("Element ID from buffer structure"),
722
+ }),
723
+ handler: async (args) => {
724
+ try {
725
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
726
+ if (!bufferInfo) {
727
+ return formatError({
728
+ error: true,
729
+ message: `Buffer not found or expired: ${args.bufferId}`,
730
+ statusCode: 404,
731
+ });
732
+ }
733
+ if (bufferInfo.metadata?.contentType !== "xhtml") {
734
+ return formatError({
735
+ error: true,
736
+ message: "Buffer is not XHTML content",
737
+ statusCode: 400,
738
+ });
739
+ }
740
+ // Get full buffer content
741
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
742
+ if (!chunk) {
743
+ return formatError({
744
+ error: true,
745
+ message: `Failed to read buffer: ${args.bufferId}`,
746
+ statusCode: 500,
747
+ });
748
+ }
749
+ // Parse and find element
750
+ const parseResult = parseXhtml(chunk.chunk);
751
+ if (!parseResult.document) {
752
+ return formatError({
753
+ error: true,
754
+ message: `Failed to parse XHTML: ${parseResult.error?.message}`,
755
+ statusCode: 400,
756
+ });
757
+ }
758
+ const root = parseResult.document.querySelector("xhtml-root");
759
+ if (!root) {
760
+ return formatError({
761
+ error: true,
762
+ message: "XHTML has no content root",
763
+ statusCode: 400,
764
+ });
765
+ }
766
+ const element = root.querySelector(`[data-jicon-id="${args.elementId}"]`);
767
+ if (!element) {
768
+ return formatError({
769
+ error: true,
770
+ message: `Element not found: ${args.elementId}`,
771
+ statusCode: 404,
772
+ details: {
773
+ availableIds: bufferInfo.structure?.map((e) => e.id) ?? [],
774
+ },
775
+ });
776
+ }
777
+ const content = element.outerHTML;
778
+ const lineCount = (content.match(/\n/g) || []).length + 1;
779
+ // Determine element type
780
+ let elementType = element.tagName.toLowerCase();
781
+ if (elementType === "ac:structured-macro") {
782
+ elementType = element.getAttribute("ac:name") || "macro";
783
+ }
784
+ return formatSuccess({
785
+ bufferId: args.bufferId,
786
+ elementId: args.elementId,
787
+ elementType,
788
+ lineCount,
789
+ content,
749
790
  });
750
791
  }
751
792
  catch (error) {
@@ -795,10 +836,24 @@ Use this to validate content before calling confluence_update_page or confluence
795
836
  statusCode: 500,
796
837
  });
797
838
  }
839
+ // Check for raw PlantUML (same check as confluence_draft_create)
840
+ // This ensures validation and draft creation agree on what's valid
841
+ const rawPlantUml = detectRawPlantUml(chunk.chunk);
798
842
  // Validate XHTML with async PlantUML validation
799
843
  const validationResult = await validateXhtmlAsync(chunk.chunk, {
800
844
  validatePlantUml: args.validatePlantUml !== false,
801
845
  });
846
+ // Add raw PlantUML error if detected
847
+ if (rawPlantUml) {
848
+ validationResult.errors.push({
849
+ type: "raw_plantuml",
850
+ message: "Content contains raw PlantUML code that is not wrapped in a Confluence macro.",
851
+ location: {
852
+ context: rawPlantUml.hint,
853
+ },
854
+ });
855
+ validationResult.valid = false;
856
+ }
802
857
  return formatSuccess({
803
858
  bufferId: args.bufferId,
804
859
  valid: validationResult.valid,