@crypto512/jicon-mcp 1.3.0 → 2.0.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 (160) hide show
  1. package/README.md +68 -85
  2. package/TOOL_LIST.md +704 -87
  3. package/dist/config/constants.d.ts +18 -7
  4. package/dist/config/constants.d.ts.map +1 -1
  5. package/dist/config/constants.js +21 -8
  6. package/dist/config/constants.js.map +1 -1
  7. package/dist/config/loader.d.ts +11 -11
  8. package/dist/config/loader.d.ts.map +1 -1
  9. package/dist/config/loader.js +53 -93
  10. package/dist/config/loader.js.map +1 -1
  11. package/dist/config/types.d.ts +3 -6
  12. package/dist/config/types.d.ts.map +1 -1
  13. package/dist/config/types.js +2 -4
  14. package/dist/config/types.js.map +1 -1
  15. package/dist/confluence/formatters.js +1 -1
  16. package/dist/confluence/formatters.js.map +1 -1
  17. package/dist/confluence/tools.d.ts +4 -0
  18. package/dist/confluence/tools.d.ts.map +1 -1
  19. package/dist/confluence/tools.js +180 -125
  20. package/dist/confluence/tools.js.map +1 -1
  21. package/dist/index.js +17 -26
  22. package/dist/index.js.map +1 -1
  23. package/dist/jira/formatters.d.ts +1 -0
  24. package/dist/jira/formatters.d.ts.map +1 -1
  25. package/dist/jira/formatters.js +13 -12
  26. package/dist/jira/formatters.js.map +1 -1
  27. package/dist/jira/tools.d.ts +4 -0
  28. package/dist/jira/tools.d.ts.map +1 -1
  29. package/dist/jira/tools.js +189 -50
  30. package/dist/jira/tools.js.map +1 -1
  31. package/dist/permissions/tool-registry.d.ts +2 -2
  32. package/dist/permissions/tool-registry.d.ts.map +1 -1
  33. package/dist/permissions/tool-registry.js +4 -2
  34. package/dist/permissions/tool-registry.js.map +1 -1
  35. package/dist/permissions/write-home-validator.d.ts.map +1 -1
  36. package/dist/permissions/write-home-validator.js +13 -3
  37. package/dist/permissions/write-home-validator.js.map +1 -1
  38. package/dist/tempo/defaults.d.ts +17 -0
  39. package/dist/tempo/defaults.d.ts.map +1 -0
  40. package/dist/tempo/defaults.js +26 -0
  41. package/dist/tempo/defaults.js.map +1 -0
  42. package/dist/tempo/tools.d.ts +5 -0
  43. package/dist/tempo/tools.d.ts.map +1 -1
  44. package/dist/tempo/tools.js +108 -34
  45. package/dist/tempo/tools.js.map +1 -1
  46. package/dist/utils/buffer-pipeline/index.d.ts +30 -0
  47. package/dist/utils/buffer-pipeline/index.d.ts.map +1 -0
  48. package/dist/utils/buffer-pipeline/index.js +317 -0
  49. package/dist/utils/buffer-pipeline/index.js.map +1 -0
  50. package/dist/utils/buffer-pipeline/output/csv.d.ts +20 -0
  51. package/dist/utils/buffer-pipeline/output/csv.d.ts.map +1 -0
  52. package/dist/utils/buffer-pipeline/output/csv.js +117 -0
  53. package/dist/utils/buffer-pipeline/output/csv.js.map +1 -0
  54. package/dist/utils/buffer-pipeline/output/json.d.ts +16 -0
  55. package/dist/utils/buffer-pipeline/output/json.d.ts.map +1 -0
  56. package/dist/utils/buffer-pipeline/output/json.js +48 -0
  57. package/dist/utils/buffer-pipeline/output/json.js.map +1 -0
  58. package/dist/utils/buffer-pipeline/output/markdown.d.ts +15 -0
  59. package/dist/utils/buffer-pipeline/output/markdown.d.ts.map +1 -0
  60. package/dist/utils/buffer-pipeline/output/markdown.js +105 -0
  61. package/dist/utils/buffer-pipeline/output/markdown.js.map +1 -0
  62. package/dist/utils/buffer-pipeline/output/xhtml-list.d.ts +16 -0
  63. package/dist/utils/buffer-pipeline/output/xhtml-list.d.ts.map +1 -0
  64. package/dist/utils/buffer-pipeline/output/xhtml-list.js +81 -0
  65. package/dist/utils/buffer-pipeline/output/xhtml-list.js.map +1 -0
  66. package/dist/utils/buffer-pipeline/output/xhtml-table.d.ts +15 -0
  67. package/dist/utils/buffer-pipeline/output/xhtml-table.d.ts.map +1 -0
  68. package/dist/utils/buffer-pipeline/output/xhtml-table.js +176 -0
  69. package/dist/utils/buffer-pipeline/output/xhtml-table.js.map +1 -0
  70. package/dist/utils/buffer-pipeline/schema.d.ts +1878 -0
  71. package/dist/utils/buffer-pipeline/schema.d.ts.map +1 -0
  72. package/dist/utils/buffer-pipeline/schema.js +168 -0
  73. package/dist/utils/buffer-pipeline/schema.js.map +1 -0
  74. package/dist/utils/buffer-pipeline/stages/filter.d.ts +32 -0
  75. package/dist/utils/buffer-pipeline/stages/filter.d.ts.map +1 -0
  76. package/dist/utils/buffer-pipeline/stages/filter.js +208 -0
  77. package/dist/utils/buffer-pipeline/stages/filter.js.map +1 -0
  78. package/dist/utils/buffer-pipeline/stages/format.d.ts +45 -0
  79. package/dist/utils/buffer-pipeline/stages/format.d.ts.map +1 -0
  80. package/dist/utils/buffer-pipeline/stages/format.js +160 -0
  81. package/dist/utils/buffer-pipeline/stages/format.js.map +1 -0
  82. package/dist/utils/buffer-pipeline/stages/group-by.d.ts +25 -0
  83. package/dist/utils/buffer-pipeline/stages/group-by.d.ts.map +1 -0
  84. package/dist/utils/buffer-pipeline/stages/group-by.js +190 -0
  85. package/dist/utils/buffer-pipeline/stages/group-by.js.map +1 -0
  86. package/dist/utils/buffer-pipeline/stages/select.d.ts +54 -0
  87. package/dist/utils/buffer-pipeline/stages/select.d.ts.map +1 -0
  88. package/dist/utils/buffer-pipeline/stages/select.js +228 -0
  89. package/dist/utils/buffer-pipeline/stages/select.js.map +1 -0
  90. package/dist/utils/buffer-pipeline/stages/sort.d.ts +20 -0
  91. package/dist/utils/buffer-pipeline/stages/sort.d.ts.map +1 -0
  92. package/dist/utils/buffer-pipeline/stages/sort.js +96 -0
  93. package/dist/utils/buffer-pipeline/stages/sort.js.map +1 -0
  94. package/dist/utils/buffer-pipeline/types.d.ts +277 -0
  95. package/dist/utils/buffer-pipeline/types.d.ts.map +1 -0
  96. package/dist/utils/buffer-pipeline/types.js +8 -0
  97. package/dist/utils/buffer-pipeline/types.js.map +1 -0
  98. package/dist/utils/buffer-tools.d.ts +749 -19
  99. package/dist/utils/buffer-tools.d.ts.map +1 -1
  100. package/dist/utils/buffer-tools.js +738 -491
  101. package/dist/utils/buffer-tools.js.map +1 -1
  102. package/dist/utils/content-buffer.d.ts +55 -4
  103. package/dist/utils/content-buffer.d.ts.map +1 -1
  104. package/dist/utils/content-buffer.js +107 -9
  105. package/dist/utils/content-buffer.js.map +1 -1
  106. package/dist/utils/jicon-help.d.ts +1 -1
  107. package/dist/utils/jicon-help.d.ts.map +1 -1
  108. package/dist/utils/jicon-help.js +253 -28
  109. package/dist/utils/jicon-help.js.map +1 -1
  110. package/dist/utils/json-structure.d.ts +121 -0
  111. package/dist/utils/json-structure.d.ts.map +1 -0
  112. package/dist/utils/json-structure.js +637 -0
  113. package/dist/utils/json-structure.js.map +1 -0
  114. package/dist/utils/plantuml/include-expander.d.ts +31 -30
  115. package/dist/utils/plantuml/include-expander.d.ts.map +1 -1
  116. package/dist/utils/plantuml/include-expander.js +167 -133
  117. package/dist/utils/plantuml/include-expander.js.map +1 -1
  118. package/dist/utils/plantuml/index.d.ts +3 -3
  119. package/dist/utils/plantuml/index.d.ts.map +1 -1
  120. package/dist/utils/plantuml/index.js +4 -4
  121. package/dist/utils/plantuml/index.js.map +1 -1
  122. package/dist/utils/plantuml/service.d.ts +13 -24
  123. package/dist/utils/plantuml/service.d.ts.map +1 -1
  124. package/dist/utils/plantuml/service.js +49 -99
  125. package/dist/utils/plantuml/service.js.map +1 -1
  126. package/dist/utils/plantuml/tools.d.ts.map +1 -1
  127. package/dist/utils/plantuml/tools.js +33 -72
  128. package/dist/utils/plantuml/tools.js.map +1 -1
  129. package/dist/utils/plantuml/types.d.ts +1 -35
  130. package/dist/utils/plantuml/types.d.ts.map +1 -1
  131. package/dist/utils/plantuml/types.js +1 -11
  132. package/dist/utils/plantuml/types.js.map +1 -1
  133. package/dist/utils/plantuml/validation-helper.d.ts +1 -1
  134. package/dist/utils/plantuml/validation-helper.js +12 -12
  135. package/dist/utils/plantuml/validation-helper.js.map +1 -1
  136. package/dist/utils/response-formatter.d.ts +61 -6
  137. package/dist/utils/response-formatter.d.ts.map +1 -1
  138. package/dist/utils/response-formatter.js +174 -91
  139. package/dist/utils/response-formatter.js.map +1 -1
  140. package/dist/utils/url-tools.d.ts.map +1 -1
  141. package/dist/utils/url-tools.js +22 -0
  142. package/dist/utils/url-tools.js.map +1 -1
  143. package/dist/utils/xhtml/error-locator.js +2 -2
  144. package/dist/utils/xhtml/error-locator.js.map +1 -1
  145. package/dist/utils/xhtml/index.d.ts +1 -1
  146. package/dist/utils/xhtml/index.d.ts.map +1 -1
  147. package/dist/utils/xhtml/index.js +1 -1
  148. package/dist/utils/xhtml/index.js.map +1 -1
  149. package/dist/utils/xhtml/parser.d.ts +34 -5
  150. package/dist/utils/xhtml/parser.d.ts.map +1 -1
  151. package/dist/utils/xhtml/parser.js +66 -11
  152. package/dist/utils/xhtml/parser.js.map +1 -1
  153. package/dist/utils/xhtml/plantuml.d.ts.map +1 -1
  154. package/dist/utils/xhtml/plantuml.js +5 -3
  155. package/dist/utils/xhtml/plantuml.js.map +1 -1
  156. package/dist/utils/xhtml/serializer.d.ts.map +1 -1
  157. package/dist/utils/xhtml/serializer.js +12 -15
  158. package/dist/utils/xhtml/serializer.js.map +1 -1
  159. package/package.json +12 -4
  160. package/crypto512-jicon-mcp-1.3.0.tgz +0 -0
@@ -2,18 +2,23 @@
2
2
  * Buffer management tools for MCP Server
3
3
  *
4
4
  * Provides tools to retrieve chunks from buffered content,
5
- * list active buffers, clear buffers, search content, edit content,
6
- * and save content to files.
5
+ * list active buffers, clear buffers, search content, and edit content.
7
6
  */
8
7
  import { z } from "zod";
9
- import * as fs from "fs";
10
- import * as path from "path";
11
8
  import { contentBuffer } from "./content-buffer.js";
12
- import { formatSuccess, formatError, getMaxOutputSize } from "./response-formatter.js";
9
+ import { formatSuccess, formatSuccessDirect, formatError, getMaxOutputSize } from "./response-formatter.js";
10
+ import { executePipelineWithOutput, validatePipeline, BufferPipelineInputSchema, } from "./buffer-pipeline/index.js";
11
+ import { createJsonStructure } from "./json-structure.js";
13
12
  import { parseXhtml, serializeXhtml, validateXhtmlAsync, buildPlantUmlMacro, detectRawPlantUml, detectPlantUmlInContent,
14
13
  // Element ID-based operations
15
14
  parseStructure, insertById, appendToDocument, replaceById, removeById, } from "./xhtml/index.js";
16
15
  import { validatePlantUmlWithFallback } from "./plantuml/index.js";
16
+ /** Default maximum characters returned per chunk retrieval */
17
+ const DEFAULT_CHUNK_SIZE = 5000;
18
+ /** Maximum items returned per buffer_get_items call */
19
+ const MAX_BATCH_ITEMS = 100;
20
+ /** Default number of items returned per buffer_get_items call */
21
+ const DEFAULT_BATCH_COUNT = 10;
17
22
  /**
18
23
  * Format grep results as text output similar to grep CLI
19
24
  */
@@ -56,47 +61,387 @@ function formatGrepOutput(result, showLineNumbers) {
56
61
  return lines.join("\n");
57
62
  }
58
63
  /**
59
- * Find project root by walking up directory tree looking for .jicon.json
64
+ * Validate buffer content type and return error if mismatch.
65
+ * Type-safe buffer access: tools should only accept buffers of the type they understand.
60
66
  */
61
- function findProjectRoot() {
62
- let dir = process.cwd();
63
- const root = path.parse(dir).root;
64
- while (dir !== root) {
65
- if (fs.existsSync(path.join(dir, ".jicon.json"))) {
66
- return dir;
67
+ function validateBufferType(bufferId, expectedTypes, toolName) {
68
+ const bufferInfo = contentBuffer.getInfo(bufferId);
69
+ if (!bufferInfo) {
70
+ return {
71
+ error: true,
72
+ result: formatError({
73
+ error: true,
74
+ message: `Buffer not found: ${bufferId}`,
75
+ statusCode: 404,
76
+ }),
77
+ };
78
+ }
79
+ const actualType = bufferInfo.metadata?.contentType;
80
+ if (!expectedTypes.includes(actualType ?? "")) {
81
+ const typeHints = {
82
+ json: "Use buffer_pipeline for JSON data, or buffer_create(contentType='json') to create JSON buffers.",
83
+ xhtml: "Use buffer_create(contentType='xhtml') to create XHTML buffers, or confluence_get_page/confluence_edit to load Confluence content.",
84
+ plain: "Use buffer_create(contentType='plain') to create plain text buffers.",
85
+ };
86
+ const suggestedTools = {
87
+ json: "buffer_pipeline, buffer_get_items",
88
+ xhtml: "buffer_edit, buffer_get_structure, buffer_get_element, confluence_draft_create",
89
+ plain: "buffer_grep, buffer_get_chunk",
90
+ };
91
+ const hint = actualType ? typeHints[actualType] || "" : "";
92
+ const suggested = actualType ? suggestedTools[actualType] || "" : "";
93
+ return {
94
+ error: true,
95
+ result: formatError({
96
+ error: true,
97
+ message: `${toolName} requires ${expectedTypes.join(" or ")} buffer, but got: ${actualType || "unknown"}`,
98
+ statusCode: 400,
99
+ details: {
100
+ bufferId,
101
+ expectedTypes,
102
+ actualType: actualType || "unknown",
103
+ hint: hint || `Create a ${expectedTypes[0]} buffer first.`,
104
+ suggestedTools: suggested ? `For ${actualType} buffers, use: ${suggested}` : undefined,
105
+ },
106
+ }),
107
+ };
108
+ }
109
+ return null; // Valid
110
+ }
111
+ /**
112
+ * Handle XHTML buffer editing with element IDs.
113
+ * Supports batch operations, PlantUML validation, and atomic rollback.
114
+ */
115
+ async function handleXhtmlEdit(args, bufferInfo) {
116
+ // Build operations list: either from operations array or single operation params
117
+ let operations;
118
+ if (args.operations && args.operations.length > 0) {
119
+ // Batch mode: use operations array
120
+ operations = args.operations;
121
+ }
122
+ else {
123
+ // Single operation mode (backwards compatible)
124
+ const hasIdOperation = args.after !== undefined || args.before !== undefined ||
125
+ args.replace !== undefined || args.append || args.remove !== undefined;
126
+ if (!hasIdOperation) {
127
+ return formatError({
128
+ error: true,
129
+ message: "XHTML buffer requires element ID operation: after, before, replace, append, remove, or operations array",
130
+ statusCode: 400,
131
+ details: {
132
+ structure: bufferInfo.structure,
133
+ nextId: bufferInfo.nextId,
134
+ hint: "Use after=ID, before=ID, replace=ID, append=true, remove=ID, or operations=[...]",
135
+ },
136
+ });
67
137
  }
68
- dir = path.dirname(dir);
138
+ operations = [{
139
+ after: args.after,
140
+ before: args.before,
141
+ replace: args.replace,
142
+ append: args.append,
143
+ remove: args.remove,
144
+ content: args.content,
145
+ plantuml: args.plantuml,
146
+ fromBufferId: args.fromBufferId,
147
+ }];
148
+ }
149
+ // PHASE 0: Resolve fromBufferId to content for all operations
150
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
151
+ const op = operations[opIndex];
152
+ if (op.fromBufferId) {
153
+ // Validate source buffer exists
154
+ const sourceInfo = contentBuffer.getInfo(op.fromBufferId);
155
+ if (!sourceInfo) {
156
+ return formatError({
157
+ error: true,
158
+ message: `Operation ${opIndex + 1}: Source buffer '${op.fromBufferId}' not found or expired`,
159
+ statusCode: 404,
160
+ });
161
+ }
162
+ // Get source buffer content
163
+ const sourceChunk = contentBuffer.getChunk(op.fromBufferId, 0, sourceInfo.bufferSizeBytes);
164
+ if (!sourceChunk) {
165
+ return formatError({
166
+ error: true,
167
+ message: `Operation ${opIndex + 1}: Failed to read source buffer '${op.fromBufferId}'`,
168
+ statusCode: 500,
169
+ });
170
+ }
171
+ // Merge content: fromBufferId content takes precedence, but can be combined with plantuml
172
+ if (!op.content) {
173
+ op.content = sourceChunk.chunk;
174
+ }
175
+ else {
176
+ // If both content and fromBufferId, concatenate
177
+ op.content = op.content + sourceChunk.chunk;
178
+ }
179
+ // Clear fromBufferId since we've resolved it
180
+ delete op.fromBufferId;
181
+ }
182
+ }
183
+ // Get buffer content
184
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.bufferSizeBytes);
185
+ if (!chunk) {
186
+ return formatError({
187
+ error: true,
188
+ message: `Failed to read buffer: ${args.bufferId}`,
189
+ statusCode: 500,
190
+ });
69
191
  }
70
- // Fallback to cwd if no marker found
71
- return process.cwd();
192
+ // Parse XHTML once
193
+ const parseResult = parseXhtml(chunk.chunk);
194
+ if (!parseResult.document) {
195
+ return formatError({
196
+ error: true,
197
+ message: `Failed to parse XHTML: ${parseResult.error?.message}`,
198
+ statusCode: 400,
199
+ });
200
+ }
201
+ const oldSize = chunk.chunk.length;
202
+ let nextId = bufferInfo.nextId ?? 1;
203
+ const allInsertedIds = [];
204
+ const diagramTypes = [];
205
+ // PHASE 1: Check for PlantUML macros in content parameters (FORBIDDEN)
206
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
207
+ const op = operations[opIndex];
208
+ if (op.content) {
209
+ const plantUmlDetection = detectPlantUmlInContent(op.content);
210
+ if (plantUmlDetection) {
211
+ return formatError({
212
+ error: true,
213
+ message: `Operation ${opIndex + 1}: PlantUML macros in 'content' parameter are forbidden. Use 'plantuml' parameter for validated insertion.`,
214
+ statusCode: 400,
215
+ details: {
216
+ operationIndex: opIndex,
217
+ macrosFound: plantUmlDetection.count,
218
+ hint: plantUmlDetection.hint,
219
+ },
220
+ });
221
+ }
222
+ }
223
+ }
224
+ // PHASE 2: Pre-validate ALL PlantUML before any DOM mutations
225
+ const validatedPlantUml = new Map();
226
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
227
+ const op = operations[opIndex];
228
+ if (op.plantuml) {
229
+ const validation = await validatePlantUmlWithFallback(op.plantuml, "insert-plantuml", {});
230
+ if (!validation.valid) {
231
+ // Type-safe error extraction
232
+ let errorData = {
233
+ error: true,
234
+ message: "PlantUML validation failed",
235
+ operationIndex: opIndex,
236
+ completedOperations: 0,
237
+ phase: "pre-validation",
238
+ };
239
+ // Safely extract error details if available
240
+ const errorObj = validation.error;
241
+ if (errorObj &&
242
+ "content" in errorObj &&
243
+ Array.isArray(errorObj.content) &&
244
+ errorObj.content[0]?.text) {
245
+ try {
246
+ const parsed = JSON.parse(errorObj.content[0].text);
247
+ errorData = {
248
+ ...parsed,
249
+ operationIndex: opIndex,
250
+ completedOperations: 0,
251
+ phase: "pre-validation",
252
+ };
253
+ }
254
+ catch {
255
+ // Keep default errorData if parsing fails
256
+ }
257
+ }
258
+ return formatError({
259
+ error: true,
260
+ message: errorData.message || "PlantUML validation failed",
261
+ statusCode: 400,
262
+ details: errorData,
263
+ });
264
+ }
265
+ validatedPlantUml.set(opIndex, validation);
266
+ }
267
+ }
268
+ // PHASE 3: Execute operations (all PlantUML pre-validated)
269
+ // Note: Rollback is implicit - buffer is only updated after ALL operations succeed
270
+ for (let opIndex = 0; opIndex < operations.length; opIndex++) {
271
+ const op = operations[opIndex];
272
+ // Build content to insert (use pre-validated PlantUML)
273
+ let contentToInsert = op.content;
274
+ let diagramType;
275
+ if (op.plantuml) {
276
+ // Use pre-validated PlantUML (already validated in PHASE 2)
277
+ const validation = validatedPlantUml.get(opIndex);
278
+ contentToInsert = buildPlantUmlMacro(validation);
279
+ diagramType = validation.diagramType;
280
+ if (diagramType)
281
+ diagramTypes.push(diagramType);
282
+ }
283
+ // Execute single operation
284
+ let result;
285
+ if (op.remove !== undefined) {
286
+ result = removeById(parseResult.document, op.remove);
287
+ }
288
+ else if (op.replace !== undefined) {
289
+ if (!contentToInsert) {
290
+ return formatError({
291
+ error: true,
292
+ message: `Operation ${opIndex + 1}: content or plantuml required for replace operation`,
293
+ statusCode: 400,
294
+ });
295
+ }
296
+ result = replaceById(parseResult.document, op.replace, contentToInsert);
297
+ }
298
+ else if (op.append) {
299
+ if (!contentToInsert) {
300
+ return formatError({
301
+ error: true,
302
+ message: `Operation ${opIndex + 1}: content or plantuml required for append operation`,
303
+ statusCode: 400,
304
+ });
305
+ }
306
+ result = appendToDocument(parseResult.document, contentToInsert, nextId);
307
+ if (result.nextId)
308
+ nextId = result.nextId;
309
+ }
310
+ else if (op.after !== undefined) {
311
+ if (!contentToInsert) {
312
+ return formatError({
313
+ error: true,
314
+ message: `Operation ${opIndex + 1}: content or plantuml required for after operation`,
315
+ statusCode: 400,
316
+ });
317
+ }
318
+ result = insertById(parseResult.document, op.after, "after", contentToInsert, nextId);
319
+ if (result.nextId)
320
+ nextId = result.nextId;
321
+ }
322
+ else if (op.before !== undefined) {
323
+ if (!contentToInsert) {
324
+ return formatError({
325
+ error: true,
326
+ message: `Operation ${opIndex + 1}: content or plantuml required for before operation`,
327
+ statusCode: 400,
328
+ });
329
+ }
330
+ result = insertById(parseResult.document, op.before, "before", contentToInsert, nextId);
331
+ if (result.nextId)
332
+ nextId = result.nextId;
333
+ }
334
+ else {
335
+ return formatError({
336
+ error: true,
337
+ message: `Operation ${opIndex + 1}: No valid operation specified (need after, before, replace, append, or remove)`,
338
+ statusCode: 400,
339
+ });
340
+ }
341
+ if (!result.success) {
342
+ // Operation failed - buffer NOT updated (atomic rollback)
343
+ return formatError({
344
+ error: true,
345
+ message: `Operation ${opIndex + 1} failed: ${result.error || "Unknown error"}. Buffer unchanged (atomic rollback).`,
346
+ statusCode: 400,
347
+ details: {
348
+ operationIndex: opIndex,
349
+ completedOperations: 0, // No operations persisted due to rollback
350
+ operation: op,
351
+ structure: bufferInfo.structure,
352
+ nextId: bufferInfo.nextId,
353
+ rollback: true,
354
+ },
355
+ });
356
+ }
357
+ // Collect inserted IDs
358
+ if (result.insertedIds) {
359
+ allInsertedIds.push(...result.insertedIds);
360
+ }
361
+ }
362
+ // Serialize and update buffer once (after all operations)
363
+ const newContent = serializeXhtml(parseResult.document);
364
+ // Rebuild structure
365
+ const newStructure = parseStructure(parseResult.document);
366
+ // Update buffer
367
+ contentBuffer.update(args.bufferId, newContent, { contentType: "xhtml" }, newStructure.structure, newStructure.nextId);
368
+ return formatSuccessDirect({
369
+ bufferId: args.bufferId,
370
+ success: true,
371
+ operationsCompleted: operations.length,
372
+ oldSize,
373
+ newSize: newContent.length,
374
+ structure: newStructure.structure,
375
+ nextId: newStructure.nextId,
376
+ ...(allInsertedIds.length > 0 && { insertedIds: allInsertedIds }),
377
+ ...(diagramTypes.length > 0 && { diagramTypes }),
378
+ }, { truncateHint: "Structure preview truncated. Use buffer_get_structure for full view." });
72
379
  }
73
380
  /**
74
- * Check if content type indicates binary data that needs base64 decoding
381
+ * Handle plain text/JSON buffer editing with string replacement or append.
75
382
  */
76
- function isBinaryContentType(contentType) {
77
- if (typeof contentType !== "string")
78
- return false;
79
- return [
80
- "image/png",
81
- "image/jpeg",
82
- "image/gif",
83
- "image/webp",
84
- "application/postscript",
85
- "application/octet-stream",
86
- ].some((type) => contentType.includes(type));
383
+ function handlePlainTextEdit(args) {
384
+ // Handle append for plain/json buffers
385
+ if (args.append && args.content) {
386
+ const result = contentBuffer.append(args.bufferId, args.content);
387
+ if (!result) {
388
+ return formatError({
389
+ error: true,
390
+ message: `Buffer not found or expired: ${args.bufferId}`,
391
+ statusCode: 404,
392
+ });
393
+ }
394
+ return formatSuccess({
395
+ bufferId: args.bufferId,
396
+ success: true,
397
+ operation: "append",
398
+ oldSize: result.oldSize,
399
+ newSize: result.newSize,
400
+ });
401
+ }
402
+ // String replacement requires old_string and new_string
403
+ if (args.old_string === undefined || args.new_string === undefined) {
404
+ return formatError({
405
+ error: true,
406
+ message: "For plain/json buffers: use old_string+new_string for replacement, or append=true+content for appending",
407
+ statusCode: 400,
408
+ details: {
409
+ hint: "To append content, use: buffer_edit(bufferId, append=true, content='text to add')",
410
+ },
411
+ });
412
+ }
413
+ const result = contentBuffer.edit(args.bufferId, args.old_string, args.new_string, args.replace_all ?? false);
414
+ if (!result) {
415
+ return formatError({
416
+ error: true,
417
+ message: `Buffer not found or expired: ${args.bufferId}`,
418
+ statusCode: 404,
419
+ });
420
+ }
421
+ if ("error" in result && result.error) {
422
+ return formatError({
423
+ error: true,
424
+ message: result.message,
425
+ statusCode: 400,
426
+ details: { occurrences: result.occurrences },
427
+ });
428
+ }
429
+ return formatSuccess(result);
87
430
  }
88
431
  export function createBufferTools() {
89
432
  return {
90
433
  buffer_create: {
91
- description: `Create a new buffer with initial content. Returns bufferId and structure (for XHTML).
434
+ description: `Create a new buffer with initial content. Returns bufferId and structure.
92
435
 
93
- TIP: Call help(topic="storage") for XHTML syntax. Call help(topic="plantuml") for diagram syntax.
94
-
95
- For XHTML content, returns structure with element IDs for use with buffer_edit:
96
- - Each element gets a unique ID (stable during session)
97
- - Use buffer_edit with after/before/replace to modify by element ID`,
436
+ Types: xhtml (Confluence storage, returns element IDs), json (data, use buffer_pipeline), plain (text).
437
+ JSON accepts string or native object/array (auto-stringified).
438
+ Run help(topic="storage") for XHTML syntax.`,
98
439
  inputSchema: z.object({
99
- content: z.string().describe("Initial content for the buffer"),
440
+ content: z.union([
441
+ z.string(),
442
+ z.array(z.unknown()),
443
+ z.record(z.unknown()),
444
+ ]).describe("Content: string for all types, or object/array for json (auto-stringified)"),
100
445
  contentType: z.enum(["xhtml", "plain", "json"]).describe("Content type: 'xhtml' for Confluence, 'plain' for text, 'json' for data"),
101
446
  metadata: z
102
447
  .record(z.unknown())
@@ -105,6 +450,22 @@ For XHTML content, returns structure with element IDs for use with buffer_edit:
105
450
  }),
106
451
  handler: async (args) => {
107
452
  try {
453
+ // Auto-stringify objects/arrays for JSON content type
454
+ let contentStr;
455
+ if (typeof args.content === "string") {
456
+ contentStr = args.content;
457
+ }
458
+ else if (args.contentType === "json") {
459
+ // Auto-stringify native objects/arrays
460
+ contentStr = JSON.stringify(args.content, null, 2);
461
+ }
462
+ else {
463
+ return formatError({
464
+ error: true,
465
+ message: `Content must be a string for contentType '${args.contentType}'. Objects/arrays are only auto-stringified for contentType 'json'.`,
466
+ statusCode: 400,
467
+ });
468
+ }
108
469
  const mergedMetadata = {
109
470
  ...args.metadata,
110
471
  contentType: args.contentType,
@@ -112,9 +473,10 @@ For XHTML content, returns structure with element IDs for use with buffer_edit:
112
473
  let bufferId;
113
474
  let structure;
114
475
  let nextId;
476
+ let jsonStructure;
115
477
  if (args.contentType === "xhtml") {
116
478
  // Check for common CDATA mistake: wrapping entire content in CDATA
117
- const trimmedContent = args.content.trim();
479
+ const trimmedContent = contentStr.trim();
118
480
  if (trimmedContent.startsWith("<![CDATA[")) {
119
481
  return formatError({
120
482
  error: true,
@@ -128,7 +490,7 @@ For XHTML content, returns structure with element IDs for use with buffer_edit:
128
490
  });
129
491
  }
130
492
  // Parse XHTML and build structure with element IDs
131
- const parseResult = parseXhtml(args.content);
493
+ const parseResult = parseXhtml(contentStr);
132
494
  if (!parseResult.document) {
133
495
  return formatError({
134
496
  error: true,
@@ -149,21 +511,67 @@ For XHTML content, returns structure with element IDs for use with buffer_edit:
149
511
  const contentWithIds = serializeXhtml(parseResult.document);
150
512
  bufferId = contentBuffer.storeWithStructure(contentWithIds, structure, nextId, mergedMetadata);
151
513
  }
514
+ else if (args.contentType === "json") {
515
+ // Parse JSON and create structure with item IDs (for arrays)
516
+ let data;
517
+ try {
518
+ data = JSON.parse(contentStr);
519
+ }
520
+ catch (e) {
521
+ return formatError({
522
+ error: true,
523
+ message: `Invalid JSON: ${e.message}`,
524
+ statusCode: 400,
525
+ });
526
+ }
527
+ // Create JSON structure (handles arrays with IDs, objects with keys)
528
+ const { content: contentWithIds, structure: jsonStruct } = createJsonStructure(data);
529
+ jsonStructure = jsonStruct;
530
+ bufferId = contentBuffer.storeJsonWithStructure(contentWithIds, jsonStruct, mergedMetadata);
531
+ }
152
532
  else {
153
- bufferId = contentBuffer.store(args.content, mergedMetadata);
533
+ bufferId = contentBuffer.store(contentStr, mergedMetadata);
154
534
  }
155
535
  const info = contentBuffer.getInfo(bufferId);
536
+ // Build response based on content type
537
+ const isArray = jsonStructure?.rootType === "array";
538
+ const isObject = jsonStructure?.rootType === "object";
539
+ const preview = isArray
540
+ ? jsonStructure?.items?.slice(0, 5).map(item => ({
541
+ id: item.id,
542
+ preview: item.preview,
543
+ }))
544
+ : undefined;
545
+ // Build hint for JSON arrays
546
+ const hint = args.contentType === "json" && isArray
547
+ ? `Use buffer_pipeline(sourceBufferId='${bufferId}', pipeline={...}) to filter, transform, and output as XHTML/CSV/JSON.`
548
+ : args.contentType === "json"
549
+ ? `Use buffer_get_chunk to read raw content, or buffer_grep to search.`
550
+ : undefined;
156
551
  return formatSuccess({
157
552
  bufferId,
158
- totalSize: info?.totalSize ?? args.content.length,
553
+ bufferSizeBytes: info?.bufferSizeBytes ?? contentStr.length,
159
554
  contentType: args.contentType,
160
555
  createdAt: info ? new Date(info.createdAt).toISOString() : new Date().toISOString(),
161
556
  expiresAt: info ? new Date(info.expiresAt).toISOString() : undefined,
162
557
  metadata: mergedMetadata,
558
+ // XHTML structure
163
559
  ...(structure && { structure, nextId }),
560
+ // JSON structure
561
+ ...(jsonStructure && {
562
+ structure: {
563
+ type: jsonStructure.rootType,
564
+ ...(isArray && { itemCount: jsonStructure.itemCount }),
565
+ ...(isArray && preview && { preview }),
566
+ ...(isObject && { keys: jsonStructure.keys }),
567
+ },
568
+ }),
569
+ ...(hint && { hint }),
164
570
  message: args.contentType === "xhtml"
165
571
  ? "Buffer created with element IDs. Use buffer_edit with after/before/replace to modify."
166
- : "Buffer created.",
572
+ : args.contentType === "json"
573
+ ? "JSON buffer created. Use buffer_pipeline for transformation."
574
+ : "Buffer created.",
167
575
  });
168
576
  }
169
577
  catch (error) {
@@ -172,7 +580,13 @@ For XHTML content, returns structure with element IDs for use with buffer_edit:
172
580
  },
173
581
  },
174
582
  buffer_get_chunk: {
175
- description: `Retrieve a chunk of buffered content by buffer ID. Use after receiving a bufferId from tools. Returns content, offset, totalSize, and hasMore flag.`,
583
+ description: `Retrieve a chunk of buffered content by buffer ID. Returns raw character-based content chunks.
584
+
585
+ WARNING: NOT available for JSON arrays or XHTML content. Use instead:
586
+ - JSON arrays: buffer_get_items(bufferId, start, count) - returns complete items
587
+ - XHTML: buffer_get_element(bufferId, elementId) - returns element content
588
+
589
+ Use buffer_get_chunk only for: plain text buffers or raw character access.`,
176
590
  inputSchema: z.object({
177
591
  bufferId: z.string().describe("Buffer ID from previous tool response"),
178
592
  offset: z
@@ -183,12 +597,49 @@ For XHTML content, returns structure with element IDs for use with buffer_edit:
183
597
  limit: z
184
598
  .number()
185
599
  .optional()
186
- .default(5000)
187
- .describe("Maximum characters to return (default: 5000)"),
600
+ .default(DEFAULT_CHUNK_SIZE)
601
+ .describe(`Maximum characters to return (default: ${DEFAULT_CHUNK_SIZE})`),
188
602
  }),
189
603
  handler: async (args) => {
190
604
  try {
191
- const chunk = contentBuffer.getChunk(args.bufferId, args.offset ?? 0, args.limit ?? 5000);
605
+ // Check buffer info to block structured content types
606
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
607
+ if (!bufferInfo) {
608
+ return formatError({
609
+ error: true,
610
+ message: `Buffer not found or expired: ${args.bufferId}`,
611
+ statusCode: 404,
612
+ });
613
+ }
614
+ const contentType = bufferInfo.metadata?.contentType;
615
+ // Block JSON arrays - force use of buffer_get_items
616
+ if (contentType === "json" && bufferInfo.jsonStructure?.rootType === "array") {
617
+ return formatError({
618
+ error: true,
619
+ message: "buffer_get_chunk is not available for JSON array buffers. Use buffer_get_items instead for complete, non-truncated items.",
620
+ statusCode: 400,
621
+ details: {
622
+ bufferId: args.bufferId,
623
+ correctTool: "buffer_get_items",
624
+ example: `buffer_get_items(bufferId="${args.bufferId}", start=0, count=50)`,
625
+ },
626
+ });
627
+ }
628
+ // Block XHTML - force use of buffer_get_element
629
+ if (contentType === "xhtml") {
630
+ return formatError({
631
+ error: true,
632
+ message: "buffer_get_chunk is not available for XHTML buffers. Use buffer_get_element for element-based access.",
633
+ statusCode: 400,
634
+ details: {
635
+ bufferId: args.bufferId,
636
+ correctTool: "buffer_get_element",
637
+ example: `buffer_get_element(bufferId="${args.bufferId}", elementId=N)`,
638
+ tip: "Use the 'structure' field in the buffer response to find element IDs",
639
+ },
640
+ });
641
+ }
642
+ const chunk = contentBuffer.getChunk(args.bufferId, args.offset ?? 0, args.limit ?? DEFAULT_CHUNK_SIZE);
192
643
  if (!chunk) {
193
644
  return formatError({
194
645
  error: true,
@@ -196,34 +647,117 @@ For XHTML content, returns structure with element IDs for use with buffer_edit:
196
647
  statusCode: 404,
197
648
  });
198
649
  }
199
- return formatSuccess({
650
+ return formatSuccessDirect({
200
651
  bufferId: chunk.bufferId,
201
652
  content: chunk.chunk,
202
653
  offset: chunk.offset,
203
654
  limit: chunk.limit,
204
- totalSize: chunk.totalSize,
655
+ bufferSizeBytes: chunk.bufferSizeBytes,
205
656
  hasMore: chunk.hasMore,
206
657
  metadata: chunk.metadata,
207
- });
658
+ }, { truncateHint: `Use smaller limit (current: ${args.limit ?? DEFAULT_CHUNK_SIZE}).` });
208
659
  }
209
660
  catch (error) {
210
661
  return formatError(error instanceof Error ? error : new Error(String(error)));
211
662
  }
212
663
  },
213
664
  },
214
- buffer_list: {
215
- description: `List all active buffers with metadata. Buffers expire after 1 hour.
665
+ buffer_get_items: {
666
+ description: `Retrieve complete JSON array items from a buffer. Use for JSON buffers from jira_search_issues, etc.
216
667
 
217
- Use to recover lost buffer IDs or find buffers associated with Confluence drafts.
668
+ Unlike buffer_get_chunk (which returns truncated raw text), this returns complete parsed items.
218
669
 
219
- Metadata includes:
220
- - resourceType: "confluence_page" for Confluence content
221
- - resourceId: page/draft ID
222
- - isDraft: true for unsaved drafts
223
- - title, spaceKey: page info
224
- - contentType: "xhtml", "plain", or "json"
670
+ Example:
671
+ jira_search_issues(jql="project = PROJ") bufferId with 500 issues
672
+ buffer_get_items(bufferId, start=0, count=10) → first 10 complete issue objects
225
673
 
226
- Example: Find buffer for draft 12345 → look for metadata.resourceId === "12345"`,
674
+ Returns: items (array), totalItems, hasMore, start, count`,
675
+ inputSchema: z.object({
676
+ bufferId: z.string().describe("Buffer ID containing JSON array"),
677
+ start: z
678
+ .number()
679
+ .optional()
680
+ .default(0)
681
+ .describe("Index to start from (default: 0)"),
682
+ count: z
683
+ .number()
684
+ .optional()
685
+ .default(DEFAULT_BATCH_COUNT)
686
+ .describe(`Number of items to return (default: ${DEFAULT_BATCH_COUNT}, max: ${MAX_BATCH_ITEMS})`),
687
+ }),
688
+ handler: async (args) => {
689
+ try {
690
+ // Type-safe buffer access: buffer_get_items requires JSON buffer
691
+ const typeError = validateBufferType(args.bufferId, ["json"], "buffer_get_items");
692
+ if (typeError)
693
+ return typeError.result;
694
+ // Get full buffer content
695
+ const bufferInfo = contentBuffer.getInfo(args.bufferId);
696
+ if (!bufferInfo) {
697
+ return formatError({
698
+ error: true,
699
+ message: `Buffer not found or expired: ${args.bufferId}`,
700
+ statusCode: 404,
701
+ });
702
+ }
703
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.bufferSizeBytes);
704
+ if (!chunk) {
705
+ return formatError({
706
+ error: true,
707
+ message: `Failed to read buffer: ${args.bufferId}`,
708
+ statusCode: 500,
709
+ });
710
+ }
711
+ // Parse JSON array
712
+ let data;
713
+ try {
714
+ const parsed = JSON.parse(chunk.chunk);
715
+ if (!Array.isArray(parsed)) {
716
+ return formatError({
717
+ error: true,
718
+ message: "Buffer content is not a JSON array",
719
+ statusCode: 400,
720
+ details: { type: typeof parsed },
721
+ });
722
+ }
723
+ data = parsed;
724
+ }
725
+ catch (e) {
726
+ return formatError({
727
+ error: true,
728
+ message: `Failed to parse JSON: ${e.message}`,
729
+ statusCode: 400,
730
+ });
731
+ }
732
+ // Slice items
733
+ const start = args.start ?? 0;
734
+ const count = Math.min(args.count ?? DEFAULT_BATCH_COUNT, MAX_BATCH_ITEMS);
735
+ const items = data.slice(start, start + count);
736
+ const hasMore = start + count < data.length;
737
+ // Get schema from first item for reference
738
+ let itemSchema;
739
+ if (data.length > 0 && typeof data[0] === "object" && data[0] !== null) {
740
+ itemSchema = Object.keys(data[0]);
741
+ }
742
+ return formatSuccess({
743
+ bufferId: args.bufferId,
744
+ items,
745
+ totalItems: data.length,
746
+ start,
747
+ count: items.length,
748
+ hasMore,
749
+ nextStart: hasMore ? start + count : undefined,
750
+ itemSchema: itemSchema ? itemSchema.slice(0, 20) : undefined, // First 20 keys
751
+ });
752
+ }
753
+ catch (error) {
754
+ return formatError(error instanceof Error ? error : new Error(String(error)));
755
+ }
756
+ },
757
+ },
758
+ buffer_list: {
759
+ description: `List all active buffers with metadata. Buffers expire after 1 hour (24h if modified).
760
+ Max 20 buffers; oldest unmodified evicted first when full. Use to recover lost buffer IDs.`,
227
761
  inputSchema: z.object({}),
228
762
  handler: async () => {
229
763
  try {
@@ -379,16 +913,11 @@ Example: Find buffer for draft 12345 → look for metadata.resourceId === "12345
379
913
  type: "text",
380
914
  text: JSON.stringify({
381
915
  _pagination: true,
382
- status: "SUCCESS - Search results buffered",
383
916
  bufferId: newBufferId,
384
- totalSize: fullText.length,
385
917
  totalMatches: grepResult.totalMatches,
386
918
  hasMore: true,
387
- next_action: {
388
- tool: "buffer_get_chunk",
389
- args: { bufferId: newBufferId, offset: 0, limit: 5000 },
390
- },
391
- }, null, 2),
919
+ next: { tool: "buffer_get_chunk", args: { bufferId: newBufferId, offset: 0, limit: DEFAULT_CHUNK_SIZE } },
920
+ }),
392
921
  },
393
922
  ],
394
923
  };
@@ -403,8 +932,8 @@ Example: Find buffer for draft 12345 → look for metadata.resourceId === "12345
403
932
  ],
404
933
  };
405
934
  }
406
- // JSON mode - use formatSuccess which handles buffering automatically
407
- return formatSuccess(grepResult);
935
+ // JSON mode - use formatSuccessDirect to avoid re-buffering
936
+ return formatSuccessDirect(grepResult, { truncateHint: "Many matches. Use more specific pattern or add head_limit parameter." });
408
937
  }
409
938
  catch (error) {
410
939
  return formatError(error instanceof Error ? error : new Error(String(error)));
@@ -412,39 +941,13 @@ Example: Find buffer for draft 12345 → look for metadata.resourceId === "12345
412
941
  },
413
942
  },
414
943
  buffer_edit: {
415
- description: `Edit buffer content. For XHTML buffers, use element IDs. For plain/json, use string replacement.
416
-
417
- TIP: Call help(topic="plantuml") BEFORE using plantuml parameter. Call help(topic="storage") for XHTML syntax.
418
-
419
- XHTML editing (by element ID) - single operation:
420
- - after: Insert content after element with this ID
421
- - before: Insert content before element with this ID
422
- - replace: Replace element with this ID
423
- - append: Add content at document end
424
- - remove: Remove element with this ID
425
- - content: XHTML to insert/replace (PlantUML macros FORBIDDEN - use plantuml parameter)
426
- - plantuml: PlantUML code (auto-wrapped in Confluence macro, validated via Docker)
427
- - fromBufferId: Insert content from another buffer (useful for composing content)
428
-
429
- XHTML batch editing - multiple operations in one call:
430
- - operations: Array of {after?, before?, replace?, append?, remove?, content?, plantuml?, fromBufferId?}
431
- - Operations are executed sequentially; stops on first error
432
- - Much more efficient than multiple tool calls (parse once, serialize once)
433
-
434
- Example batch: operations=[{after:6, plantuml:"@startuml..."}, {after:8, fromBufferId:"buf_xxx"}]
435
-
436
- EDITING EXISTING PAGES with separate buffer:
437
- 1. confluence_edit(URL) → bufferId_A (page content with structure)
438
- 2. buffer_create(content) → bufferId_B (your new content)
439
- 3. buffer_edit(bufferId=bufferId_A, after=X, fromBufferId=bufferId_B) → inserts B into A
440
- 4. confluence_draft_create(pageId, bufferId_A) → creates draft with combined content
944
+ description: `Edit buffer content by element ID (XHTML) or string match (plain/JSON).
441
945
 
442
- Plain/JSON editing (string replacement):
443
- - old_string: Text to replace
444
- - new_string: Replacement text
445
- - replace_all: Replace all occurrences
946
+ XHTML: after/before/replace/remove by element ID. content="<p>...</p>" | plantuml="@startuml..." | fromBufferId="buf_xxx"
947
+ BATCH: operations=[{after:6,content:"<p>A</p>"},{remove:8}] one call instead of many.
948
+ Plain/JSON: old_string + new_string, or append=true + content.
446
949
 
447
- Returns updated structure for XHTML buffers.`,
950
+ Run help(topic="buffers") for editing guide and examples.`,
448
951
  inputSchema: z.object({
449
952
  bufferId: z.string().describe("Buffer ID to modify"),
450
953
  // XHTML batch operations
@@ -483,293 +986,11 @@ Returns updated structure for XHTML buffers.`,
483
986
  });
484
987
  }
485
988
  const isXhtml = bufferInfo.metadata?.contentType === "xhtml";
486
- // XHTML editing with element IDs
989
+ // Dispatch to content-type-specific handler
487
990
  if (isXhtml) {
488
- let operations;
489
- if (args.operations && args.operations.length > 0) {
490
- // Batch mode: use operations array
491
- operations = args.operations;
492
- }
493
- else {
494
- // Single operation mode (backwards compatible)
495
- const hasIdOperation = args.after !== undefined || args.before !== undefined ||
496
- args.replace !== undefined || args.append || args.remove !== undefined;
497
- if (!hasIdOperation) {
498
- return formatError({
499
- error: true,
500
- message: "XHTML buffer requires element ID operation: after, before, replace, append, remove, or operations array",
501
- statusCode: 400,
502
- details: {
503
- structure: bufferInfo.structure,
504
- nextId: bufferInfo.nextId,
505
- hint: "Use after=ID, before=ID, replace=ID, append=true, remove=ID, or operations=[...]",
506
- },
507
- });
508
- }
509
- operations = [{
510
- after: args.after,
511
- before: args.before,
512
- replace: args.replace,
513
- append: args.append,
514
- remove: args.remove,
515
- content: args.content,
516
- plantuml: args.plantuml,
517
- fromBufferId: args.fromBufferId,
518
- }];
519
- }
520
- // PHASE 0: Resolve fromBufferId to content for all operations
521
- for (let opIndex = 0; opIndex < operations.length; opIndex++) {
522
- const op = operations[opIndex];
523
- if (op.fromBufferId) {
524
- // Validate source buffer exists
525
- const sourceInfo = contentBuffer.getInfo(op.fromBufferId);
526
- if (!sourceInfo) {
527
- return formatError({
528
- error: true,
529
- message: `Operation ${opIndex + 1}: Source buffer '${op.fromBufferId}' not found or expired`,
530
- statusCode: 404,
531
- });
532
- }
533
- // Get source buffer content
534
- const sourceChunk = contentBuffer.getChunk(op.fromBufferId, 0, sourceInfo.totalSize);
535
- if (!sourceChunk) {
536
- return formatError({
537
- error: true,
538
- message: `Operation ${opIndex + 1}: Failed to read source buffer '${op.fromBufferId}'`,
539
- statusCode: 500,
540
- });
541
- }
542
- // Merge content: fromBufferId content takes precedence, but can be combined with plantuml
543
- if (!op.content) {
544
- op.content = sourceChunk.chunk;
545
- }
546
- else {
547
- // If both content and fromBufferId, concatenate
548
- op.content = op.content + sourceChunk.chunk;
549
- }
550
- // Clear fromBufferId since we've resolved it
551
- delete op.fromBufferId;
552
- }
553
- }
554
- // Get buffer content
555
- const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
556
- if (!chunk) {
557
- return formatError({
558
- error: true,
559
- message: `Failed to read buffer: ${args.bufferId}`,
560
- statusCode: 500,
561
- });
562
- }
563
- // Parse XHTML once
564
- const parseResult = parseXhtml(chunk.chunk);
565
- if (!parseResult.document) {
566
- return formatError({
567
- error: true,
568
- message: `Failed to parse XHTML: ${parseResult.error?.message}`,
569
- statusCode: 400,
570
- });
571
- }
572
- const oldSize = chunk.chunk.length;
573
- let nextId = bufferInfo.nextId ?? 1;
574
- const allInsertedIds = [];
575
- const diagramTypes = [];
576
- // PHASE 1: Check for PlantUML macros in content parameters (FORBIDDEN)
577
- for (let opIndex = 0; opIndex < operations.length; opIndex++) {
578
- const op = operations[opIndex];
579
- if (op.content) {
580
- const plantUmlDetection = detectPlantUmlInContent(op.content);
581
- if (plantUmlDetection) {
582
- return formatError({
583
- error: true,
584
- message: `Operation ${opIndex + 1}: PlantUML macros in 'content' parameter are forbidden. Use 'plantuml' parameter for validated insertion.`,
585
- statusCode: 400,
586
- details: {
587
- operationIndex: opIndex,
588
- macrosFound: plantUmlDetection.count,
589
- hint: plantUmlDetection.hint,
590
- },
591
- });
592
- }
593
- }
594
- }
595
- // PHASE 2: Pre-validate ALL PlantUML before any DOM mutations
596
- const validatedPlantUml = new Map();
597
- for (let opIndex = 0; opIndex < operations.length; opIndex++) {
598
- const op = operations[opIndex];
599
- if (op.plantuml) {
600
- const validation = await validatePlantUmlWithFallback(op.plantuml, "insert-plantuml", {});
601
- if (!validation.valid) {
602
- // Type-safe error extraction
603
- let errorData = {
604
- error: true,
605
- message: "PlantUML validation failed",
606
- operationIndex: opIndex,
607
- completedOperations: 0,
608
- phase: "pre-validation",
609
- };
610
- // Safely extract error details if available
611
- const errorObj = validation.error;
612
- if (errorObj &&
613
- "content" in errorObj &&
614
- Array.isArray(errorObj.content) &&
615
- errorObj.content[0]?.text) {
616
- try {
617
- const parsed = JSON.parse(errorObj.content[0].text);
618
- errorData = {
619
- ...parsed,
620
- operationIndex: opIndex,
621
- completedOperations: 0,
622
- phase: "pre-validation",
623
- };
624
- }
625
- catch {
626
- // Keep default errorData if parsing fails
627
- }
628
- }
629
- return {
630
- content: [{ type: "text", text: JSON.stringify(errorData, null, 2) }],
631
- };
632
- }
633
- validatedPlantUml.set(opIndex, validation);
634
- }
635
- }
636
- // PHASE 3: Execute operations (all PlantUML pre-validated)
637
- // Note: Rollback is implicit - buffer is only updated after ALL operations succeed
638
- for (let opIndex = 0; opIndex < operations.length; opIndex++) {
639
- const op = operations[opIndex];
640
- // Build content to insert (use pre-validated PlantUML)
641
- let contentToInsert = op.content;
642
- let diagramType;
643
- if (op.plantuml) {
644
- // Use pre-validated PlantUML (already validated in PHASE 2)
645
- const validation = validatedPlantUml.get(opIndex);
646
- contentToInsert = buildPlantUmlMacro(validation);
647
- diagramType = validation.diagramType;
648
- if (diagramType)
649
- diagramTypes.push(diagramType);
650
- }
651
- // Execute single operation
652
- let result;
653
- if (op.remove !== undefined) {
654
- result = removeById(parseResult.document, op.remove);
655
- }
656
- else if (op.replace !== undefined) {
657
- if (!contentToInsert) {
658
- return formatError({
659
- error: true,
660
- message: `Operation ${opIndex + 1}: content or plantuml required for replace operation`,
661
- statusCode: 400,
662
- });
663
- }
664
- result = replaceById(parseResult.document, op.replace, contentToInsert);
665
- }
666
- else if (op.append) {
667
- if (!contentToInsert) {
668
- return formatError({
669
- error: true,
670
- message: `Operation ${opIndex + 1}: content or plantuml required for append operation`,
671
- statusCode: 400,
672
- });
673
- }
674
- result = appendToDocument(parseResult.document, contentToInsert, nextId);
675
- if (result.nextId)
676
- nextId = result.nextId;
677
- }
678
- else if (op.after !== undefined) {
679
- if (!contentToInsert) {
680
- return formatError({
681
- error: true,
682
- message: `Operation ${opIndex + 1}: content or plantuml required for after operation`,
683
- statusCode: 400,
684
- });
685
- }
686
- result = insertById(parseResult.document, op.after, "after", contentToInsert, nextId);
687
- if (result.nextId)
688
- nextId = result.nextId;
689
- }
690
- else if (op.before !== undefined) {
691
- if (!contentToInsert) {
692
- return formatError({
693
- error: true,
694
- message: `Operation ${opIndex + 1}: content or plantuml required for before operation`,
695
- statusCode: 400,
696
- });
697
- }
698
- result = insertById(parseResult.document, op.before, "before", contentToInsert, nextId);
699
- if (result.nextId)
700
- nextId = result.nextId;
701
- }
702
- else {
703
- return formatError({
704
- error: true,
705
- message: `Operation ${opIndex + 1}: No valid operation specified (need after, before, replace, append, or remove)`,
706
- statusCode: 400,
707
- });
708
- }
709
- if (!result.success) {
710
- // Operation failed - buffer NOT updated (atomic rollback)
711
- return formatError({
712
- error: true,
713
- message: `Operation ${opIndex + 1} failed: ${result.error || "Unknown error"}. Buffer unchanged (atomic rollback).`,
714
- statusCode: 400,
715
- details: {
716
- operationIndex: opIndex,
717
- completedOperations: 0, // No operations persisted due to rollback
718
- operation: op,
719
- structure: bufferInfo.structure,
720
- nextId: bufferInfo.nextId,
721
- rollback: true,
722
- },
723
- });
724
- }
725
- // Collect inserted IDs
726
- if (result.insertedIds) {
727
- allInsertedIds.push(...result.insertedIds);
728
- }
729
- }
730
- // Serialize and update buffer once (after all operations)
731
- const newContent = serializeXhtml(parseResult.document);
732
- // Rebuild structure
733
- const newStructure = parseStructure(parseResult.document);
734
- // Update buffer
735
- contentBuffer.update(args.bufferId, newContent, { contentType: "xhtml" }, newStructure.structure, newStructure.nextId);
736
- return formatSuccess({
737
- bufferId: args.bufferId,
738
- success: true,
739
- operationsCompleted: operations.length,
740
- oldSize,
741
- newSize: newContent.length,
742
- structure: newStructure.structure,
743
- nextId: newStructure.nextId,
744
- ...(allInsertedIds.length > 0 && { insertedIds: allInsertedIds }),
745
- ...(diagramTypes.length > 0 && { diagramTypes }),
746
- });
991
+ return handleXhtmlEdit(args, bufferInfo);
747
992
  }
748
- // Plain text/JSON editing
749
- if (args.old_string === undefined || args.new_string === undefined) {
750
- return formatError({
751
- error: true,
752
- message: "old_string and new_string required for plain/json buffer editing",
753
- statusCode: 400,
754
- });
755
- }
756
- const result = contentBuffer.edit(args.bufferId, args.old_string, args.new_string, args.replace_all ?? false);
757
- if (!result) {
758
- return formatError({
759
- error: true,
760
- message: `Buffer not found or expired: ${args.bufferId}`,
761
- statusCode: 404,
762
- });
763
- }
764
- if ("error" in result && result.error) {
765
- return formatError({
766
- error: true,
767
- message: result.message,
768
- statusCode: 400,
769
- details: { occurrences: result.occurrences },
770
- });
771
- }
772
- return formatSuccess(result);
993
+ return handlePlainTextEdit(args);
773
994
  }
774
995
  catch (error) {
775
996
  return formatError(error instanceof Error ? error : new Error(String(error)));
@@ -789,29 +1010,15 @@ Each element has:
789
1010
  }),
790
1011
  handler: async (args) => {
791
1012
  try {
1013
+ // Type-safe buffer access: buffer_get_structure requires XHTML buffer
1014
+ const typeError = validateBufferType(args.bufferId, ["xhtml"], "buffer_get_structure");
1015
+ if (typeError)
1016
+ return typeError.result;
792
1017
  const bufferInfo = contentBuffer.getInfo(args.bufferId);
793
- if (!bufferInfo) {
794
- return formatError({
795
- error: true,
796
- message: `Buffer not found or expired: ${args.bufferId}`,
797
- statusCode: 404,
798
- });
799
- }
800
- if (bufferInfo.metadata?.contentType !== "xhtml") {
801
- return formatError({
802
- error: true,
803
- message: "Buffer is not XHTML content",
804
- statusCode: 400,
805
- details: {
806
- contentType: bufferInfo.metadata?.contentType,
807
- hint: "Structure is only available for XHTML buffers",
808
- },
809
- });
810
- }
811
1018
  return formatSuccess({
812
1019
  bufferId: args.bufferId,
813
- structure: bufferInfo.structure ?? [],
814
- nextId: bufferInfo.nextId ?? 1,
1020
+ structure: bufferInfo?.structure ?? [],
1021
+ nextId: bufferInfo?.nextId ?? 1,
815
1022
  });
816
1023
  }
817
1024
  catch (error) {
@@ -833,6 +1040,10 @@ Example: After a Confluence error at element 12, use this to see the content:
833
1040
  }),
834
1041
  handler: async (args) => {
835
1042
  try {
1043
+ // Type-safe buffer access: buffer_get_element requires XHTML buffer
1044
+ const typeError = validateBufferType(args.bufferId, ["xhtml"], "buffer_get_element");
1045
+ if (typeError)
1046
+ return typeError.result;
836
1047
  const bufferInfo = contentBuffer.getInfo(args.bufferId);
837
1048
  if (!bufferInfo) {
838
1049
  return formatError({
@@ -841,15 +1052,8 @@ Example: After a Confluence error at element 12, use this to see the content:
841
1052
  statusCode: 404,
842
1053
  });
843
1054
  }
844
- if (bufferInfo.metadata?.contentType !== "xhtml") {
845
- return formatError({
846
- error: true,
847
- message: "Buffer is not XHTML content",
848
- statusCode: 400,
849
- });
850
- }
851
1055
  // Get full buffer content
852
- const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
1056
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.bufferSizeBytes);
853
1057
  if (!chunk) {
854
1058
  return formatError({
855
1059
  error: true,
@@ -906,20 +1110,8 @@ Example: After a Confluence error at element 12, use this to see the content:
906
1110
  },
907
1111
  },
908
1112
  buffer_validate_xhtml: {
909
- description: `Validate buffered content as Confluence storage format (XHTML).
910
-
911
- Checks:
912
- - XML well-formedness (balanced tags, proper nesting)
913
- - Required attributes on Confluence elements (ac:name, ri:space-key, etc.)
914
- - Valid layout section types (single, two_equal, etc.)
915
- - Known macro names (warns for unknown macros)
916
- - PlantUML syntax (via Docker service, if running)
917
-
918
- PlantUML validation:
919
- - If Docker service is running, validates all PlantUML macros
920
- - If not running, adds warning (use plantuml_validate to start the service)
921
-
922
- Use this to validate content before calling confluence_update_page or confluence_create_page.`,
1113
+ description: `Validate buffered XHTML as Confluence storage format. Checks well-formedness, required attributes, macro names, and PlantUML syntax (if Docker running).
1114
+ Use before confluence_draft_create to catch errors early.`,
923
1115
  inputSchema: z.object({
924
1116
  bufferId: z.string().describe("Buffer ID containing XHTML content"),
925
1117
  validatePlantUml: z
@@ -930,6 +1122,10 @@ Use this to validate content before calling confluence_update_page or confluence
930
1122
  }),
931
1123
  handler: async (args) => {
932
1124
  try {
1125
+ // Type-safe buffer access: buffer_validate_xhtml requires XHTML buffer
1126
+ const typeError = validateBufferType(args.bufferId, ["xhtml"], "buffer_validate_xhtml");
1127
+ if (typeError)
1128
+ return typeError.result;
933
1129
  // Get buffer content
934
1130
  const bufferInfo = contentBuffer.getInfo(args.bufferId);
935
1131
  if (!bufferInfo) {
@@ -939,7 +1135,7 @@ Use this to validate content before calling confluence_update_page or confluence
939
1135
  statusCode: 404,
940
1136
  });
941
1137
  }
942
- const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
1138
+ const chunk = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.bufferSizeBytes);
943
1139
  if (!chunk) {
944
1140
  return formatError({
945
1141
  error: true,
@@ -988,91 +1184,142 @@ Use this to validate content before calling confluence_update_page or confluence
988
1184
  }
989
1185
  },
990
1186
  },
991
- buffer_save_to_file: {
992
- description: `Save buffer content to a file within the project directory.
1187
+ // =========================================================================
1188
+ // Buffer Pipeline - Unified data transformation tool
1189
+ // =========================================================================
1190
+ buffer_pipeline: {
1191
+ description: `Transform JSON array with declarative pipeline (server-side, 98% token reduction).
993
1192
 
994
- SECURITY: Files can only be saved within the project root (directory containing .jicon.json).
995
- Attempts to write outside this directory will be rejected.
1193
+ STAGES: select filter groupBy sort limit format → output (required)
1194
+ OUTPUT: xhtml_table | xhtml_list | json | csv | markdown
1195
+ NOTE: xhtml/csv/md outputs are NOT iterable (buffer_get_items won't work). json IS iterable.
1196
+ RETURNS: bufferId, contentType, itemCount
996
1197
 
997
- Binary handling:
998
- - If buffer metadata indicates binary content (image/png, application/postscript, etc.),
999
- content is automatically decoded from base64
1000
- - Use decodeBase64=true to force base64 decoding
1001
- - PNG and EPS from plantuml_render are base64 encoded and will be auto-decoded
1198
+ Example: pipeline={filter:[{field:"status.name",operator:"ne",value:"Done"}],output:{type:"xhtml_table",table:{columns:[{field:"key"},{field:"summary"}]}}}
1002
1199
 
1003
- Example:
1004
- buffer_save_to_file(bufferId="buf_xxx", outputPath="./diagrams/sequence.png")
1005
- buffer_save_to_file(bufferId="buf_yyy", outputPath="output/diagram.svg")`,
1006
- inputSchema: z.object({
1007
- bufferId: z.string().describe("Buffer ID containing content to save"),
1008
- outputPath: z
1009
- .string()
1010
- .describe("Output file path relative to project root, or absolute path within project"),
1011
- decodeBase64: z
1012
- .boolean()
1013
- .optional()
1014
- .describe("Force base64 decoding (auto-detected for binary content types like PNG/EPS)"),
1015
- }),
1200
+ IMPORTANT: pipeline must be object (not string). Use "sort" (not "sortBy"). output is REQUIRED.
1201
+ Run help(topic="buffers") for full schema, operators, and examples.`,
1202
+ inputSchema: BufferPipelineInputSchema,
1016
1203
  handler: async (args) => {
1017
1204
  try {
1018
- // 1. Validate buffer exists
1019
- const info = contentBuffer.getInfo(args.bufferId);
1020
- if (!info) {
1205
+ // Type-safe buffer access: buffer_pipeline requires JSON buffer
1206
+ const typeError = validateBufferType(args.sourceBufferId, ["json"], "buffer_pipeline");
1207
+ if (typeError)
1208
+ return typeError.result;
1209
+ // 1. Get buffer info
1210
+ const bufferInfo = contentBuffer.getInfo(args.sourceBufferId);
1211
+ if (!bufferInfo) {
1021
1212
  return formatError({
1022
1213
  error: true,
1023
- message: `Buffer not found: ${args.bufferId}`,
1214
+ message: `Buffer not found or expired: ${args.sourceBufferId}`,
1024
1215
  statusCode: 404,
1025
1216
  });
1026
1217
  }
1027
- // 2. Find project root
1028
- const projectRoot = findProjectRoot();
1029
- // 3. Resolve and validate output path
1030
- const resolvedPath = path.resolve(projectRoot, args.outputPath);
1031
- const normalizedProjectRoot = path.normalize(projectRoot);
1032
- const normalizedOutputPath = path.normalize(resolvedPath);
1033
- // Security check: ensure path is within project directory
1034
- // Must start with projectRoot + separator, or be exactly projectRoot
1035
- if (!normalizedOutputPath.startsWith(normalizedProjectRoot + path.sep) &&
1036
- normalizedOutputPath !== normalizedProjectRoot) {
1218
+ // 2. Get buffer content
1219
+ const chunk = contentBuffer.getChunk(args.sourceBufferId, 0, bufferInfo.bufferSizeBytes);
1220
+ if (!chunk) {
1037
1221
  return formatError({
1038
1222
  error: true,
1039
- message: "Security: output path must be within project directory",
1040
- statusCode: 403,
1041
- details: {
1042
- projectRoot: normalizedProjectRoot,
1043
- requestedPath: normalizedOutputPath,
1044
- hint: "Use a path relative to project root (e.g., './output/file.png')",
1045
- },
1223
+ message: `Failed to read buffer content: ${args.sourceBufferId}`,
1224
+ statusCode: 500,
1046
1225
  });
1047
1226
  }
1048
- // 4. Get buffer content
1049
- const chunk = contentBuffer.getChunk(args.bufferId, 0, info.totalSize);
1050
- if (!chunk) {
1227
+ // 3. Parse JSON array
1228
+ let data;
1229
+ try {
1230
+ const parsed = JSON.parse(chunk.chunk);
1231
+ if (!Array.isArray(parsed)) {
1232
+ return formatError({
1233
+ error: true,
1234
+ message: "Buffer content is not a JSON array. buffer_pipeline requires array data.",
1235
+ statusCode: 400,
1236
+ details: { type: typeof parsed },
1237
+ });
1238
+ }
1239
+ data = parsed;
1240
+ }
1241
+ catch (e) {
1051
1242
  return formatError({
1052
1243
  error: true,
1053
- message: `Failed to read buffer: ${args.bufferId}`,
1054
- statusCode: 500,
1244
+ message: `Failed to parse buffer as JSON: ${e.message}`,
1245
+ statusCode: 400,
1055
1246
  });
1056
1247
  }
1057
- // 5. Determine if base64 decoding needed
1058
- const shouldDecode = args.decodeBase64 === true ||
1059
- (args.decodeBase64 !== false &&
1060
- isBinaryContentType(info.metadata?.contentType));
1061
- // 6. Prepare content
1062
- const content = shouldDecode
1063
- ? Buffer.from(chunk.chunk, "base64")
1064
- : Buffer.from(chunk.chunk, "utf-8");
1065
- // 7. Ensure parent directory exists
1066
- const parentDir = path.dirname(resolvedPath);
1067
- await fs.promises.mkdir(parentDir, { recursive: true });
1068
- // 8. Write file
1069
- await fs.promises.writeFile(resolvedPath, content);
1070
- return formatSuccess({
1071
- success: true,
1072
- path: resolvedPath,
1073
- size: content.length,
1074
- decoded: shouldDecode,
1075
- message: `File saved successfully to ${resolvedPath}`,
1248
+ // 4. Build pipeline context
1249
+ const context = {
1250
+ sourceBufferId: args.sourceBufferId,
1251
+ jiraUrl: process.env.JIRA_URL,
1252
+ confluenceUrl: process.env.CONFLUENCE_URL,
1253
+ metadata: bufferInfo.metadata,
1254
+ };
1255
+ // 5. Dry run - validate only
1256
+ if (args.dryRun) {
1257
+ const validation = validatePipeline(data, args.pipeline);
1258
+ return formatSuccessDirect({
1259
+ dryRun: true,
1260
+ valid: validation.valid,
1261
+ errors: validation.errors,
1262
+ warnings: validation.warnings,
1263
+ availableFields: validation.availableFields,
1264
+ itemCount: data.length,
1265
+ });
1266
+ }
1267
+ // 6. Execute pipeline
1268
+ const pipelineResult = executePipelineWithOutput(data, args.pipeline, context);
1269
+ if ("error" in pipelineResult) {
1270
+ return formatError(pipelineResult.error);
1271
+ }
1272
+ // 7. Store output in buffer
1273
+ const { result, output } = pipelineResult;
1274
+ const outputType = args.pipeline.output.type;
1275
+ // Determine content type for buffer
1276
+ const contentType = outputType.startsWith("xhtml")
1277
+ ? "xhtml"
1278
+ : outputType === "json"
1279
+ ? "json"
1280
+ : "plain";
1281
+ const bufferMetadata = {
1282
+ contentType,
1283
+ resourceType: "pipeline_output",
1284
+ sourceBufferId: args.sourceBufferId,
1285
+ pipelineStages: result.pipelineStages,
1286
+ outputType,
1287
+ };
1288
+ let newBufferId;
1289
+ if (contentType === "xhtml") {
1290
+ // Parse XHTML to get structure with element IDs
1291
+ const parseResult = parseXhtml(output);
1292
+ if (parseResult.document) {
1293
+ const structureResult = parseStructure(parseResult.document);
1294
+ // Re-serialize to include data-jicon-id attributes
1295
+ const contentWithIds = serializeXhtml(parseResult.document);
1296
+ newBufferId = contentBuffer.storeWithStructure(contentWithIds, structureResult.structure, structureResult.nextId, bufferMetadata);
1297
+ }
1298
+ else {
1299
+ // Fallback: store without structure if parsing fails
1300
+ newBufferId = contentBuffer.store(output, bufferMetadata);
1301
+ }
1302
+ }
1303
+ else if (contentType === "json") {
1304
+ // Parse JSON to get structure with item IDs
1305
+ try {
1306
+ const data = JSON.parse(output);
1307
+ const { content: contentWithIds, structure: jsonStruct } = createJsonStructure(data);
1308
+ newBufferId = contentBuffer.storeJsonWithStructure(contentWithIds, jsonStruct, bufferMetadata);
1309
+ }
1310
+ catch {
1311
+ // Fallback: store without structure if parsing fails
1312
+ newBufferId = contentBuffer.store(output, bufferMetadata);
1313
+ }
1314
+ }
1315
+ else {
1316
+ newBufferId = contentBuffer.store(output, bufferMetadata);
1317
+ }
1318
+ // 8. Return result with bufferId
1319
+ return formatSuccessDirect({
1320
+ ...result,
1321
+ bufferId: newBufferId,
1322
+ contentType,
1076
1323
  });
1077
1324
  }
1078
1325
  catch (error) {