@crypto512/jicon-mcp 1.2.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.
- package/README.md +73 -67
- package/TOOL_LIST.md +785 -133
- package/dist/config/constants.d.ts +18 -7
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/constants.js +21 -8
- package/dist/config/constants.js.map +1 -1
- package/dist/config/loader.d.ts +11 -11
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +53 -93
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +3 -6
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +2 -4
- package/dist/config/types.js.map +1 -1
- package/dist/confluence/formatters.js +1 -1
- package/dist/confluence/formatters.js.map +1 -1
- package/dist/confluence/tools.d.ts +8 -12
- package/dist/confluence/tools.d.ts.map +1 -1
- package/dist/confluence/tools.js +285 -233
- package/dist/confluence/tools.js.map +1 -1
- package/dist/index.js +17 -26
- package/dist/index.js.map +1 -1
- package/dist/jira/formatters.d.ts +1 -0
- package/dist/jira/formatters.d.ts.map +1 -1
- package/dist/jira/formatters.js +13 -12
- package/dist/jira/formatters.js.map +1 -1
- package/dist/jira/tools.d.ts +4 -0
- package/dist/jira/tools.d.ts.map +1 -1
- package/dist/jira/tools.js +234 -44
- package/dist/jira/tools.js.map +1 -1
- package/dist/permissions/tool-registry.d.ts +2 -2
- package/dist/permissions/tool-registry.d.ts.map +1 -1
- package/dist/permissions/tool-registry.js +4 -2
- package/dist/permissions/tool-registry.js.map +1 -1
- package/dist/permissions/write-home-validator.d.ts.map +1 -1
- package/dist/permissions/write-home-validator.js +13 -3
- package/dist/permissions/write-home-validator.js.map +1 -1
- package/dist/tempo/defaults.d.ts +17 -0
- package/dist/tempo/defaults.d.ts.map +1 -0
- package/dist/tempo/defaults.js +26 -0
- package/dist/tempo/defaults.js.map +1 -0
- package/dist/tempo/tools.d.ts +5 -0
- package/dist/tempo/tools.d.ts.map +1 -1
- package/dist/tempo/tools.js +161 -35
- package/dist/tempo/tools.js.map +1 -1
- package/dist/utils/buffer-pipeline/index.d.ts +30 -0
- package/dist/utils/buffer-pipeline/index.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/index.js +317 -0
- package/dist/utils/buffer-pipeline/index.js.map +1 -0
- package/dist/utils/buffer-pipeline/output/csv.d.ts +20 -0
- package/dist/utils/buffer-pipeline/output/csv.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/output/csv.js +117 -0
- package/dist/utils/buffer-pipeline/output/csv.js.map +1 -0
- package/dist/utils/buffer-pipeline/output/json.d.ts +16 -0
- package/dist/utils/buffer-pipeline/output/json.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/output/json.js +48 -0
- package/dist/utils/buffer-pipeline/output/json.js.map +1 -0
- package/dist/utils/buffer-pipeline/output/markdown.d.ts +15 -0
- package/dist/utils/buffer-pipeline/output/markdown.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/output/markdown.js +105 -0
- package/dist/utils/buffer-pipeline/output/markdown.js.map +1 -0
- package/dist/utils/buffer-pipeline/output/xhtml-list.d.ts +16 -0
- package/dist/utils/buffer-pipeline/output/xhtml-list.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/output/xhtml-list.js +81 -0
- package/dist/utils/buffer-pipeline/output/xhtml-list.js.map +1 -0
- package/dist/utils/buffer-pipeline/output/xhtml-table.d.ts +15 -0
- package/dist/utils/buffer-pipeline/output/xhtml-table.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/output/xhtml-table.js +176 -0
- package/dist/utils/buffer-pipeline/output/xhtml-table.js.map +1 -0
- package/dist/utils/buffer-pipeline/schema.d.ts +1878 -0
- package/dist/utils/buffer-pipeline/schema.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/schema.js +168 -0
- package/dist/utils/buffer-pipeline/schema.js.map +1 -0
- package/dist/utils/buffer-pipeline/stages/filter.d.ts +32 -0
- package/dist/utils/buffer-pipeline/stages/filter.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/stages/filter.js +208 -0
- package/dist/utils/buffer-pipeline/stages/filter.js.map +1 -0
- package/dist/utils/buffer-pipeline/stages/format.d.ts +45 -0
- package/dist/utils/buffer-pipeline/stages/format.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/stages/format.js +160 -0
- package/dist/utils/buffer-pipeline/stages/format.js.map +1 -0
- package/dist/utils/buffer-pipeline/stages/group-by.d.ts +25 -0
- package/dist/utils/buffer-pipeline/stages/group-by.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/stages/group-by.js +190 -0
- package/dist/utils/buffer-pipeline/stages/group-by.js.map +1 -0
- package/dist/utils/buffer-pipeline/stages/select.d.ts +54 -0
- package/dist/utils/buffer-pipeline/stages/select.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/stages/select.js +228 -0
- package/dist/utils/buffer-pipeline/stages/select.js.map +1 -0
- package/dist/utils/buffer-pipeline/stages/sort.d.ts +20 -0
- package/dist/utils/buffer-pipeline/stages/sort.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/stages/sort.js +96 -0
- package/dist/utils/buffer-pipeline/stages/sort.js.map +1 -0
- package/dist/utils/buffer-pipeline/types.d.ts +277 -0
- package/dist/utils/buffer-pipeline/types.d.ts.map +1 -0
- package/dist/utils/buffer-pipeline/types.js +8 -0
- package/dist/utils/buffer-pipeline/types.js.map +1 -0
- package/dist/utils/buffer-tools.d.ts +749 -19
- package/dist/utils/buffer-tools.d.ts.map +1 -1
- package/dist/utils/buffer-tools.js +738 -491
- package/dist/utils/buffer-tools.js.map +1 -1
- package/dist/utils/content-buffer.d.ts +55 -4
- package/dist/utils/content-buffer.d.ts.map +1 -1
- package/dist/utils/content-buffer.js +107 -9
- package/dist/utils/content-buffer.js.map +1 -1
- package/dist/utils/jicon-help.d.ts +1 -1
- package/dist/utils/jicon-help.d.ts.map +1 -1
- package/dist/utils/jicon-help.js +345 -99
- package/dist/utils/jicon-help.js.map +1 -1
- package/dist/utils/json-structure.d.ts +121 -0
- package/dist/utils/json-structure.d.ts.map +1 -0
- package/dist/utils/json-structure.js +637 -0
- package/dist/utils/json-structure.js.map +1 -0
- package/dist/utils/plantuml/include-expander.d.ts +31 -30
- package/dist/utils/plantuml/include-expander.d.ts.map +1 -1
- package/dist/utils/plantuml/include-expander.js +167 -133
- package/dist/utils/plantuml/include-expander.js.map +1 -1
- package/dist/utils/plantuml/index.d.ts +3 -3
- package/dist/utils/plantuml/index.d.ts.map +1 -1
- package/dist/utils/plantuml/index.js +4 -4
- package/dist/utils/plantuml/index.js.map +1 -1
- package/dist/utils/plantuml/service.d.ts +13 -24
- package/dist/utils/plantuml/service.d.ts.map +1 -1
- package/dist/utils/plantuml/service.js +49 -99
- package/dist/utils/plantuml/service.js.map +1 -1
- package/dist/utils/plantuml/tools.d.ts.map +1 -1
- package/dist/utils/plantuml/tools.js +33 -72
- package/dist/utils/plantuml/tools.js.map +1 -1
- package/dist/utils/plantuml/types.d.ts +1 -35
- package/dist/utils/plantuml/types.d.ts.map +1 -1
- package/dist/utils/plantuml/types.js +1 -11
- package/dist/utils/plantuml/types.js.map +1 -1
- package/dist/utils/plantuml/validation-helper.d.ts +1 -1
- package/dist/utils/plantuml/validation-helper.js +12 -12
- package/dist/utils/plantuml/validation-helper.js.map +1 -1
- package/dist/utils/response-formatter.d.ts +68 -0
- package/dist/utils/response-formatter.d.ts.map +1 -1
- package/dist/utils/response-formatter.js +186 -78
- package/dist/utils/response-formatter.js.map +1 -1
- package/dist/utils/url-tools.d.ts.map +1 -1
- package/dist/utils/url-tools.js +22 -0
- package/dist/utils/url-tools.js.map +1 -1
- package/dist/utils/xhtml/error-locator.js +2 -2
- package/dist/utils/xhtml/error-locator.js.map +1 -1
- package/dist/utils/xhtml/index.d.ts +1 -1
- package/dist/utils/xhtml/index.d.ts.map +1 -1
- package/dist/utils/xhtml/index.js +1 -1
- package/dist/utils/xhtml/index.js.map +1 -1
- package/dist/utils/xhtml/parser.d.ts +34 -5
- package/dist/utils/xhtml/parser.d.ts.map +1 -1
- package/dist/utils/xhtml/parser.js +66 -11
- package/dist/utils/xhtml/parser.js.map +1 -1
- package/dist/utils/xhtml/plantuml.d.ts.map +1 -1
- package/dist/utils/xhtml/plantuml.js +5 -3
- package/dist/utils/xhtml/plantuml.js.map +1 -1
- package/dist/utils/xhtml/serializer.d.ts.map +1 -1
- package/dist/utils/xhtml/serializer.js +12 -15
- package/dist/utils/xhtml/serializer.js.map +1 -1
- package/dist/utils/xhtml/types.d.ts +1 -0
- package/dist/utils/xhtml/types.d.ts.map +1 -1
- package/package.json +12 -4
|
@@ -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
|
-
*
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
71
|
-
|
|
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
|
-
*
|
|
381
|
+
* Handle plain text/JSON buffer editing with string replacement or append.
|
|
75
382
|
*/
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
434
|
+
description: `Create a new buffer with initial content. Returns bufferId and structure.
|
|
92
435
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
:
|
|
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.
|
|
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(
|
|
187
|
-
.describe(
|
|
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
|
-
|
|
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
|
|
650
|
+
return formatSuccessDirect({
|
|
200
651
|
bufferId: chunk.bufferId,
|
|
201
652
|
content: chunk.chunk,
|
|
202
653
|
offset: chunk.offset,
|
|
203
654
|
limit: chunk.limit,
|
|
204
|
-
|
|
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
|
-
|
|
215
|
-
description: `
|
|
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
|
-
|
|
668
|
+
Unlike buffer_get_chunk (which returns truncated raw text), this returns complete parsed items.
|
|
218
669
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
|
407
|
-
return
|
|
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
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
989
|
+
// Dispatch to content-type-specific handler
|
|
487
990
|
if (isXhtml) {
|
|
488
|
-
|
|
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
|
-
|
|
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
|
|
814
|
-
nextId: bufferInfo
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
992
|
-
|
|
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
|
-
|
|
995
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
//
|
|
1019
|
-
const
|
|
1020
|
-
if (
|
|
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.
|
|
1214
|
+
message: `Buffer not found or expired: ${args.sourceBufferId}`,
|
|
1024
1215
|
statusCode: 404,
|
|
1025
1216
|
});
|
|
1026
1217
|
}
|
|
1027
|
-
// 2.
|
|
1028
|
-
const
|
|
1029
|
-
|
|
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:
|
|
1040
|
-
statusCode:
|
|
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
|
-
//
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
|
1054
|
-
statusCode:
|
|
1244
|
+
message: `Failed to parse buffer as JSON: ${e.message}`,
|
|
1245
|
+
statusCode: 400,
|
|
1055
1246
|
});
|
|
1056
1247
|
}
|
|
1057
|
-
//
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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) {
|