@crypto512/jicon-mcp 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROMPT.md +214 -0
- package/README.md +77 -8
- package/TOOL_LIST.md +319 -101
- package/crypto512-jicon-mcp-1.3.0.tgz +0 -0
- package/dist/confluence/client.d.ts +7 -2
- package/dist/confluence/client.d.ts.map +1 -1
- package/dist/confluence/client.js +28 -10
- package/dist/confluence/client.js.map +1 -1
- package/dist/confluence/tools.d.ts +68 -20
- package/dist/confluence/tools.d.ts.map +1 -1
- package/dist/confluence/tools.js +682 -131
- package/dist/confluence/tools.js.map +1 -1
- package/dist/jira/tools.d.ts.map +1 -1
- package/dist/jira/tools.js +76 -25
- package/dist/jira/tools.js.map +1 -1
- package/dist/permissions/tool-registry.d.ts +9 -9
- package/dist/permissions/tool-registry.d.ts.map +1 -1
- package/dist/permissions/tool-registry.js +10 -3
- 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 +23 -3
- package/dist/permissions/write-home-validator.js.map +1 -1
- package/dist/tempo/client.js +1 -1
- package/dist/tempo/client.js.map +1 -1
- package/dist/tempo/tools.d.ts.map +1 -1
- package/dist/tempo/tools.js +75 -23
- package/dist/tempo/tools.js.map +1 -1
- package/dist/utils/buffer-tools.d.ts +10 -0
- package/dist/utils/buffer-tools.d.ts.map +1 -1
- package/dist/utils/buffer-tools.js +139 -28
- package/dist/utils/buffer-tools.js.map +1 -1
- package/dist/utils/content-buffer.d.ts +5 -1
- package/dist/utils/content-buffer.d.ts.map +1 -1
- package/dist/utils/content-buffer.js +6 -3
- 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 +158 -78
- package/dist/utils/jicon-help.js.map +1 -1
- package/dist/utils/plantuml/client.d.ts +15 -1
- package/dist/utils/plantuml/client.d.ts.map +1 -1
- package/dist/utils/plantuml/client.js +56 -3
- package/dist/utils/plantuml/client.js.map +1 -1
- package/dist/utils/plantuml/include-expander.d.ts +15 -0
- package/dist/utils/plantuml/include-expander.d.ts.map +1 -1
- package/dist/utils/plantuml/include-expander.js +47 -8
- package/dist/utils/plantuml/include-expander.js.map +1 -1
- package/dist/utils/plantuml/index.d.ts +1 -1
- package/dist/utils/plantuml/index.d.ts.map +1 -1
- package/dist/utils/plantuml/index.js +1 -1
- package/dist/utils/plantuml/index.js.map +1 -1
- package/dist/utils/plantuml/service.d.ts +1 -1
- package/dist/utils/plantuml/service.d.ts.map +1 -1
- package/dist/utils/plantuml/service.js +1 -1
- 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 +5 -2
- package/dist/utils/plantuml/tools.js.map +1 -1
- package/dist/utils/response-formatter.d.ts +13 -0
- package/dist/utils/response-formatter.d.ts.map +1 -1
- package/dist/utils/response-formatter.js +25 -0
- package/dist/utils/response-formatter.js.map +1 -1
- package/dist/utils/url-tools.d.ts +27 -1
- package/dist/utils/url-tools.d.ts.map +1 -1
- package/dist/utils/url-tools.js +142 -1
- package/dist/utils/url-tools.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/plantuml.d.ts +24 -6
- package/dist/utils/xhtml/plantuml.d.ts.map +1 -1
- package/dist/utils/xhtml/plantuml.js +70 -12
- package/dist/utils/xhtml/plantuml.js.map +1 -1
- package/dist/utils/xhtml/types.d.ts +1 -0
- package/dist/utils/xhtml/types.d.ts.map +1 -1
- package/dist/utils/xhtml/validator.js +2 -2
- package/dist/utils/xhtml/validator.js.map +1 -1
- package/package.json +2 -2
package/dist/confluence/tools.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* Confluence MCP Tools
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { formatSuccess, formatError, isApiError } from "../utils/response-formatter.js";
|
|
5
|
+
import { formatSuccess, formatSuccessBuffered, formatError, isApiError } from "../utils/response-formatter.js";
|
|
6
6
|
import { contentBuffer } from "../utils/content-buffer.js";
|
|
7
7
|
import { formatPageMetadata } from "./formatters.js";
|
|
8
8
|
import { validateXhtmlAsync, parseXhtml, parseStructure, serializeXhtml, enhanceXhtmlError } from "../utils/xhtml/index.js";
|
|
9
9
|
import { detectRawPlantUml, detectDiagramType } from "../utils/xhtml/plantuml.js";
|
|
10
|
-
import { expandPlantUmlInXhtml } from "../utils/plantuml/index.js";
|
|
10
|
+
import { expandPlantUmlInXhtml, collapseExpandedIncludesInXhtml } from "../utils/plantuml/index.js";
|
|
11
|
+
import { parseUrl } from "../utils/url-tools.js";
|
|
12
|
+
import { DEFAULT_PAGE_EXPAND } from "./defaults.js";
|
|
11
13
|
/**
|
|
12
14
|
* Generate a summary of content structure for draft responses.
|
|
13
15
|
* Helps AI assistants verify that diagrams and other elements are included.
|
|
@@ -44,72 +46,17 @@ function getContentSummary(content) {
|
|
|
44
46
|
result.headingCount = doc.querySelectorAll('h1, h2, h3, h4, h5, h6').length;
|
|
45
47
|
return result;
|
|
46
48
|
}
|
|
47
|
-
/**
|
|
48
|
-
* Resolve content from either direct content string or bufferId.
|
|
49
|
-
* Returns { content, error } - if error is set, return it as the tool result.
|
|
50
|
-
*/
|
|
51
|
-
function resolveContentFromBuffer(contentArg, bufferIdArg) {
|
|
52
|
-
// Exactly one of content or bufferId must be provided
|
|
53
|
-
if (contentArg && bufferIdArg) {
|
|
54
|
-
return {
|
|
55
|
-
error: formatError({
|
|
56
|
-
error: true,
|
|
57
|
-
message: "Provide either 'content' or 'bufferId', not both",
|
|
58
|
-
statusCode: 400,
|
|
59
|
-
}),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
if (bufferIdArg) {
|
|
63
|
-
const bufferChunk = contentBuffer.getChunk(bufferIdArg);
|
|
64
|
-
if (!bufferChunk) {
|
|
65
|
-
return {
|
|
66
|
-
error: formatError({
|
|
67
|
-
error: true,
|
|
68
|
-
message: `Buffer ${bufferIdArg} not found or expired`,
|
|
69
|
-
statusCode: 404,
|
|
70
|
-
}),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
// Validate that buffer contains XHTML content for Confluence
|
|
74
|
-
const bufferInfo = contentBuffer.getInfo(bufferIdArg);
|
|
75
|
-
if (bufferInfo?.metadata?.contentType && bufferInfo.metadata.contentType !== "xhtml") {
|
|
76
|
-
return {
|
|
77
|
-
error: formatError({
|
|
78
|
-
error: true,
|
|
79
|
-
message: `Buffer ${bufferIdArg} contains ${bufferInfo.metadata.contentType} content, but Confluence requires XHTML.`,
|
|
80
|
-
statusCode: 400,
|
|
81
|
-
details: {
|
|
82
|
-
hint: "Create Confluence content with buffer_create, then edit with buffer_edit.",
|
|
83
|
-
foundContentType: bufferInfo.metadata.contentType,
|
|
84
|
-
expectedContentType: "xhtml",
|
|
85
|
-
},
|
|
86
|
-
}),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
// Get full content from buffer
|
|
90
|
-
const fullContent = contentBuffer.getChunk(bufferIdArg, 0, bufferChunk.totalSize);
|
|
91
|
-
if (!fullContent) {
|
|
92
|
-
return {
|
|
93
|
-
error: formatError({
|
|
94
|
-
error: true,
|
|
95
|
-
message: "Failed to retrieve full buffer content",
|
|
96
|
-
statusCode: 500,
|
|
97
|
-
}),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
return { content: fullContent.chunk };
|
|
101
|
-
}
|
|
102
|
-
// Direct content provided
|
|
103
|
-
return { content: contentArg };
|
|
104
|
-
}
|
|
105
49
|
/**
|
|
106
50
|
* Validate XHTML content before Confluence write operations.
|
|
107
51
|
* Returns error result if validation fails, null if valid.
|
|
52
|
+
* When bufferId is provided, includes it in error response for recovery.
|
|
108
53
|
*/
|
|
109
|
-
async function validateContentForWrite(content) {
|
|
54
|
+
async function validateContentForWrite(content, bufferId) {
|
|
110
55
|
const validation = await validateXhtmlAsync(content, { validatePlantUml: true });
|
|
111
56
|
if (!validation.valid) {
|
|
112
57
|
const errorMessages = [];
|
|
58
|
+
let errorElementId;
|
|
59
|
+
let errorContext;
|
|
113
60
|
// XHTML structure errors
|
|
114
61
|
if (validation.errors && validation.errors.length > 0) {
|
|
115
62
|
errorMessages.push("XHTML validation errors:");
|
|
@@ -122,6 +69,14 @@ async function validateContentForWrite(content) {
|
|
|
122
69
|
? err.location.context.substring(0, 60) + "..."
|
|
123
70
|
: err.location.context;
|
|
124
71
|
errorMessages.push(` Near: "${contextPreview}"`);
|
|
72
|
+
// Capture first error context for recovery
|
|
73
|
+
if (!errorContext) {
|
|
74
|
+
errorContext = err.location.context;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Capture first error elementId for recovery
|
|
78
|
+
if (!errorElementId && err.location?.elementId) {
|
|
79
|
+
errorElementId = err.location.elementId;
|
|
125
80
|
}
|
|
126
81
|
});
|
|
127
82
|
}
|
|
@@ -149,10 +104,18 @@ async function validateContentForWrite(content) {
|
|
|
149
104
|
errorMessages.push(" </ac:structured-macro>");
|
|
150
105
|
}
|
|
151
106
|
}
|
|
152
|
-
// Add
|
|
107
|
+
// Add recovery instructions
|
|
153
108
|
errorMessages.push("");
|
|
154
|
-
|
|
155
|
-
|
|
109
|
+
if (bufferId) {
|
|
110
|
+
errorMessages.push(`RECOVERY: Use buffer_edit(bufferId="${bufferId}", ...) to fix errors.`);
|
|
111
|
+
if (errorElementId) {
|
|
112
|
+
errorMessages.push(` buffer_edit(bufferId="${bufferId}", replace=${errorElementId}, content="<fixed>...</fixed>")`);
|
|
113
|
+
}
|
|
114
|
+
else if (errorContext) {
|
|
115
|
+
errorMessages.push(` Use buffer_grep(bufferId="${bufferId}", pattern="...") to find the error location.`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
errorMessages.push('TIP: Call help(topic="storage") for XHTML syntax (HTML vs XHTML differences).');
|
|
156
119
|
errorMessages.push("");
|
|
157
120
|
errorMessages.push("ACTION REQUIRED: Fix content errors before calling this tool again.");
|
|
158
121
|
errorMessages.push("DO NOT claim success - the draft was NOT created.");
|
|
@@ -162,7 +125,12 @@ async function validateContentForWrite(content) {
|
|
|
162
125
|
statusCode: 400,
|
|
163
126
|
details: {
|
|
164
127
|
validationErrors: errorMessages,
|
|
165
|
-
|
|
128
|
+
...(bufferId && { bufferId }),
|
|
129
|
+
...(errorElementId && { errorElementId }),
|
|
130
|
+
...(errorContext && { errorContext }),
|
|
131
|
+
hint: errorElementId
|
|
132
|
+
? `Use buffer_edit(bufferId="${bufferId}", replace=${errorElementId}, content="...") to fix`
|
|
133
|
+
: `Use buffer_grep to find the error, then buffer_edit to fix`,
|
|
166
134
|
},
|
|
167
135
|
});
|
|
168
136
|
}
|
|
@@ -183,7 +151,9 @@ function storeXhtmlWithStructure(content, metadata) {
|
|
|
183
151
|
export function createConfluenceTools(client) {
|
|
184
152
|
return {
|
|
185
153
|
confluence_search_content: {
|
|
186
|
-
description: `Search Confluence content using CQL. Auto-fetches all results
|
|
154
|
+
description: `Search Confluence content using CQL. Auto-fetches all results.
|
|
155
|
+
|
|
156
|
+
Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.
|
|
187
157
|
|
|
188
158
|
TIP: See help(topic="cql") for CQL syntax guide.
|
|
189
159
|
|
|
@@ -197,8 +167,10 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
|
|
|
197
167
|
handler: async (args) => {
|
|
198
168
|
try {
|
|
199
169
|
const result = await client.searchContentAll(args.cql, args.expand);
|
|
200
|
-
|
|
201
|
-
|
|
170
|
+
return formatSuccessBuffered(result, {
|
|
171
|
+
resourceType: "confluence_search",
|
|
172
|
+
title: `CQL: ${args.cql.substring(0, 100)}${args.cql.length > 100 ? "..." : ""}`,
|
|
173
|
+
});
|
|
202
174
|
}
|
|
203
175
|
catch (error) {
|
|
204
176
|
// Enhanced error handling for common CQL errors
|
|
@@ -225,7 +197,7 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
|
|
|
225
197
|
hint += "- ✅ RIGHT: (text~\"a\" OR text~\"b\") - Each term needs its own text~\n";
|
|
226
198
|
}
|
|
227
199
|
hint += "\nExamples:\n";
|
|
228
|
-
hint += " text~\"
|
|
200
|
+
hint += " text~\"Mike Tasc\" - finds pages mentioning this person\n";
|
|
229
201
|
hint += " text~\"meeting\" AND space=MESH - finds meetings in MESH space\n";
|
|
230
202
|
hint += " title~\"sprint review\" - finds pages with sprint review in title";
|
|
231
203
|
return formatError({
|
|
@@ -256,7 +228,7 @@ WARNING: Use text~ (not content~ or body~). Use space KEY (not name).`,
|
|
|
256
228
|
PREFERRED method when you have a page ID from search results. Faster and more reliable than get_page_by_title.
|
|
257
229
|
|
|
258
230
|
Returns pageId, version, bufferId, and structure (element IDs) for structured editing.
|
|
259
|
-
Use buffer_edit
|
|
231
|
+
Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.`,
|
|
260
232
|
inputSchema: z.object({
|
|
261
233
|
pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
|
|
262
234
|
expand: z
|
|
@@ -267,7 +239,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
267
239
|
handler: async (args) => {
|
|
268
240
|
try {
|
|
269
241
|
const result = await client.getPage(args.pageId, args.expand);
|
|
270
|
-
const
|
|
242
|
+
const rawContent = result.body?.storage?.value || "";
|
|
243
|
+
// Collapse expanded includes back to !include directives
|
|
244
|
+
const content = collapseExpandedIncludesInXhtml(rawContent);
|
|
271
245
|
// Store content with element IDs for structured editing
|
|
272
246
|
const { bufferId, structure } = storeXhtmlWithStructure(content, {
|
|
273
247
|
resourceType: "confluence_page",
|
|
@@ -284,7 +258,7 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
284
258
|
bufferId,
|
|
285
259
|
...(structure && { structure }),
|
|
286
260
|
contentSize: content.length,
|
|
287
|
-
message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create for user review.",
|
|
261
|
+
message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
|
|
288
262
|
});
|
|
289
263
|
}
|
|
290
264
|
catch (error) {
|
|
@@ -298,7 +272,7 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
298
272
|
Use ONLY when you don't have a page ID. IMPORTANT: Use the space KEY (e.g. 'MESH', 'TC'), NOT the space name.
|
|
299
273
|
|
|
300
274
|
Returns pageId, version, bufferId, and structure (element IDs) for structured editing.
|
|
301
|
-
Use buffer_edit
|
|
275
|
+
Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.`,
|
|
302
276
|
inputSchema: z.object({
|
|
303
277
|
spaceKey: z.string().describe("Space key (short code like 'MESH', 'TC'), NOT the full space name"),
|
|
304
278
|
title: z.string().describe("Page title"),
|
|
@@ -314,7 +288,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
314
288
|
statusCode: 404,
|
|
315
289
|
});
|
|
316
290
|
}
|
|
317
|
-
const
|
|
291
|
+
const rawContent = result.body?.storage?.value || "";
|
|
292
|
+
// Collapse expanded includes back to !include directives
|
|
293
|
+
const content = collapseExpandedIncludesInXhtml(rawContent);
|
|
318
294
|
// Store content with element IDs for structured editing
|
|
319
295
|
const { bufferId, structure } = storeXhtmlWithStructure(content, {
|
|
320
296
|
resourceType: "confluence_page",
|
|
@@ -331,7 +307,196 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
331
307
|
bufferId,
|
|
332
308
|
...(structure && { structure }),
|
|
333
309
|
contentSize: content.length,
|
|
334
|
-
message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create for user review.",
|
|
310
|
+
message: "Page loaded. Use buffer_edit to modify, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
confluence_edit: {
|
|
319
|
+
description: `Smart page/draft loader - auto-resolves URLs, pageIds, draftIds, or SPACE/Title.
|
|
320
|
+
|
|
321
|
+
ACCEPTS ANY OF:
|
|
322
|
+
- Full URL: https://confluence.example.com/pages/viewpage.action?pageId=123
|
|
323
|
+
- Full URL: https://confluence.example.com/pages/resumedraft.action?draftId=456
|
|
324
|
+
- Full URL: https://confluence.example.com/display/SPACE/Page+Title
|
|
325
|
+
- Page ID: "123456"
|
|
326
|
+
- Draft ID: "draft:123456" (prefix with "draft:")
|
|
327
|
+
- Space/Title: "DOCS/API Guide"
|
|
328
|
+
|
|
329
|
+
SMART BEHAVIOR:
|
|
330
|
+
- URLs are parsed automatically to extract pageId or draftId
|
|
331
|
+
- Draft IDs: tries to load draft; if 404 (published), finds page by title
|
|
332
|
+
- Returns bufferId + structure + pageId for editing
|
|
333
|
+
|
|
334
|
+
WORKFLOW:
|
|
335
|
+
1. confluence_edit(input) → bufferId, structure, pageId
|
|
336
|
+
2. buffer_edit(bufferId, ...) → modify content
|
|
337
|
+
3. confluence_draft_create(pageId=..., bufferId=...) → draft linked to original page
|
|
338
|
+
4. User publishes via Confluence UI (updates original page)
|
|
339
|
+
5. For more edits: confluence_edit(same URL or "SPACE/Title") → auto-resolves`,
|
|
340
|
+
inputSchema: z.object({
|
|
341
|
+
input: z.string().describe('URL, pageId, "draft:ID", or "SPACE/Title"'),
|
|
342
|
+
}),
|
|
343
|
+
handler: async (args) => {
|
|
344
|
+
const input = args.input.trim();
|
|
345
|
+
if (!input) {
|
|
346
|
+
return formatError({
|
|
347
|
+
error: true,
|
|
348
|
+
message: "Input is required. Provide a URL, pageId, draft:ID, or SPACE/Title.",
|
|
349
|
+
statusCode: 400,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// Helper to load page content and return formatted result
|
|
353
|
+
const loadPageContent = async (pageResult) => {
|
|
354
|
+
const rawContent = pageResult.body?.storage?.value || "";
|
|
355
|
+
const content = collapseExpandedIncludesInXhtml(rawContent);
|
|
356
|
+
const { bufferId, structure } = storeXhtmlWithStructure(content, {
|
|
357
|
+
resourceType: "confluence_page",
|
|
358
|
+
resourceId: String(pageResult.id),
|
|
359
|
+
contentType: "xhtml",
|
|
360
|
+
version: pageResult.version?.number,
|
|
361
|
+
spaceKey: pageResult.space?.key,
|
|
362
|
+
title: pageResult.title,
|
|
363
|
+
});
|
|
364
|
+
return {
|
|
365
|
+
pageId: pageResult.id,
|
|
366
|
+
spaceKey: pageResult.space?.key,
|
|
367
|
+
title: pageResult.title,
|
|
368
|
+
version: pageResult.version?.number,
|
|
369
|
+
bufferId,
|
|
370
|
+
structure,
|
|
371
|
+
contentSize: content.length,
|
|
372
|
+
message: "Content loaded. Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to add content, then confluence_draft_create(pageId=..., bufferId=...) for user review.",
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
// Helper to load draft content
|
|
376
|
+
const loadDraftContent = async (draftId) => {
|
|
377
|
+
const result = await client.getDraft(draftId);
|
|
378
|
+
const rawContent = result.body?.storage?.value || "";
|
|
379
|
+
const content = collapseExpandedIncludesInXhtml(rawContent);
|
|
380
|
+
const { bufferId, structure } = storeXhtmlWithStructure(content, {
|
|
381
|
+
resourceType: "confluence_page",
|
|
382
|
+
resourceId: String(result.id),
|
|
383
|
+
contentType: "xhtml",
|
|
384
|
+
spaceKey: result.space?.key,
|
|
385
|
+
title: result.title,
|
|
386
|
+
isDraft: true,
|
|
387
|
+
});
|
|
388
|
+
return {
|
|
389
|
+
draftId: result.id,
|
|
390
|
+
spaceKey: result.space?.key,
|
|
391
|
+
title: result.title,
|
|
392
|
+
bufferId,
|
|
393
|
+
structure,
|
|
394
|
+
contentSize: content.length,
|
|
395
|
+
message: "Draft loaded. Use buffer_edit to modify, then confluence_draft_save to update.",
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
try {
|
|
399
|
+
// Case 1: Full URL
|
|
400
|
+
if (input.startsWith("http://") || input.startsWith("https://")) {
|
|
401
|
+
const parsed = parseUrl(input);
|
|
402
|
+
if (parsed.type === "confluence_page" && parsed.pageId) {
|
|
403
|
+
const result = await client.getPage(parsed.pageId);
|
|
404
|
+
return formatSuccess(await loadPageContent(result));
|
|
405
|
+
}
|
|
406
|
+
if (parsed.type === "confluence_draft" && parsed.draftId) {
|
|
407
|
+
try {
|
|
408
|
+
return formatSuccess(await loadDraftContent(parsed.draftId));
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
// Draft may have been published - try to find page by title
|
|
412
|
+
if (isApiError(error) && error.statusCode === 404 && parsed.spaceKey && parsed.title) {
|
|
413
|
+
const pageResult = await client.getPageByTitle(parsed.spaceKey, parsed.title);
|
|
414
|
+
if (pageResult) {
|
|
415
|
+
return formatSuccess({
|
|
416
|
+
...await loadPageContent(pageResult),
|
|
417
|
+
note: "Draft was published. Loaded the published page instead.",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (parsed.type === "confluence_space_path" && parsed.spaceKey && parsed.title) {
|
|
425
|
+
const pageResult = await client.getPageByTitle(parsed.spaceKey, parsed.title);
|
|
426
|
+
if (!pageResult) {
|
|
427
|
+
return formatError({
|
|
428
|
+
error: true,
|
|
429
|
+
message: `Page not found: "${parsed.title}" in space ${parsed.spaceKey}`,
|
|
430
|
+
statusCode: 404,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return formatSuccess(await loadPageContent(pageResult));
|
|
434
|
+
}
|
|
435
|
+
return formatError({
|
|
436
|
+
error: true,
|
|
437
|
+
message: `Unable to parse URL: "${input}". Use Confluence page, draft, or display URL.`,
|
|
438
|
+
statusCode: 400,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
// Case 2: Draft ID (prefix with "draft:")
|
|
442
|
+
if (input.startsWith("draft:")) {
|
|
443
|
+
const draftId = input.substring(6);
|
|
444
|
+
try {
|
|
445
|
+
return formatSuccess(await loadDraftContent(draftId));
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
if (isApiError(error) && error.statusCode === 404) {
|
|
449
|
+
return formatError({
|
|
450
|
+
error: true,
|
|
451
|
+
message: `Draft ${draftId} not found. It may have been published. Try using the page URL or "SPACE/Title" format.`,
|
|
452
|
+
statusCode: 404,
|
|
453
|
+
details: {
|
|
454
|
+
hint: "If the draft was published, ask the user for the published page URL.",
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
throw error;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Case 3: Space/Title format (e.g., "DOCS/API Guide")
|
|
462
|
+
if (input.includes("/") && !input.match(/^\d+$/)) {
|
|
463
|
+
const slashIndex = input.indexOf("/");
|
|
464
|
+
const spaceKey = input.substring(0, slashIndex).trim();
|
|
465
|
+
const title = input.substring(slashIndex + 1).trim();
|
|
466
|
+
if (!spaceKey || !title) {
|
|
467
|
+
return formatError({
|
|
468
|
+
error: true,
|
|
469
|
+
message: `Invalid SPACE/Title format: "${input}". Expected "SPACEKEY/Page Title".`,
|
|
470
|
+
statusCode: 400,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
const pageResult = await client.getPageByTitle(spaceKey, title);
|
|
474
|
+
if (!pageResult) {
|
|
475
|
+
return formatError({
|
|
476
|
+
error: true,
|
|
477
|
+
message: `Page not found: "${title}" in space ${spaceKey}`,
|
|
478
|
+
statusCode: 404,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return formatSuccess(await loadPageContent(pageResult));
|
|
482
|
+
}
|
|
483
|
+
// Case 4: Pure numeric page ID
|
|
484
|
+
if (/^\d+$/.test(input)) {
|
|
485
|
+
const result = await client.getPage(input);
|
|
486
|
+
return formatSuccess(await loadPageContent(result));
|
|
487
|
+
}
|
|
488
|
+
return formatError({
|
|
489
|
+
error: true,
|
|
490
|
+
message: `Unable to parse input: "${input}". Expected URL, pageId, "draft:ID", or "SPACE/Title".`,
|
|
491
|
+
statusCode: 400,
|
|
492
|
+
details: {
|
|
493
|
+
examples: [
|
|
494
|
+
"https://confluence.example.com/pages/viewpage.action?pageId=123",
|
|
495
|
+
"123456 (page ID)",
|
|
496
|
+
"draft:456789 (draft ID)",
|
|
497
|
+
"DOCS/API Guide (space/title)",
|
|
498
|
+
],
|
|
499
|
+
},
|
|
335
500
|
});
|
|
336
501
|
}
|
|
337
502
|
catch (error) {
|
|
@@ -366,7 +531,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
366
531
|
},
|
|
367
532
|
},
|
|
368
533
|
confluence_list_spaces: {
|
|
369
|
-
description: `List all accessible Confluence spaces
|
|
534
|
+
description: `List all accessible Confluence spaces.
|
|
535
|
+
|
|
536
|
+
Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
|
|
370
537
|
inputSchema: z.object({
|
|
371
538
|
type: z
|
|
372
539
|
.enum(["global", "personal"])
|
|
@@ -376,7 +543,10 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
376
543
|
handler: async (args) => {
|
|
377
544
|
try {
|
|
378
545
|
const result = await client.listSpaces(args.type);
|
|
379
|
-
return
|
|
546
|
+
return formatSuccessBuffered(result, {
|
|
547
|
+
resourceType: "confluence_spaces",
|
|
548
|
+
title: args.type ? `${args.type} spaces` : "All Spaces",
|
|
549
|
+
});
|
|
380
550
|
}
|
|
381
551
|
catch (error) {
|
|
382
552
|
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
@@ -384,7 +554,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
384
554
|
},
|
|
385
555
|
},
|
|
386
556
|
confluence_get_space: {
|
|
387
|
-
description:
|
|
557
|
+
description: `Get detailed information about a space.
|
|
558
|
+
|
|
559
|
+
Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
|
|
388
560
|
inputSchema: z.object({
|
|
389
561
|
spaceKey: z.string().describe("Space key"),
|
|
390
562
|
expand: z.array(z.string()).optional().describe("Additional data to expand"),
|
|
@@ -392,8 +564,11 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
392
564
|
handler: async (args) => {
|
|
393
565
|
try {
|
|
394
566
|
const result = await client.getSpace(args.spaceKey, args.expand);
|
|
395
|
-
|
|
396
|
-
|
|
567
|
+
return formatSuccessBuffered(result, {
|
|
568
|
+
resourceType: "confluence_space",
|
|
569
|
+
title: result.name || args.spaceKey,
|
|
570
|
+
spaceKey: args.spaceKey,
|
|
571
|
+
});
|
|
397
572
|
}
|
|
398
573
|
catch (error) {
|
|
399
574
|
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
@@ -401,7 +576,9 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
401
576
|
},
|
|
402
577
|
},
|
|
403
578
|
confluence_get_page_children: {
|
|
404
|
-
description: `Get all child pages of a page
|
|
579
|
+
description: `Get all child pages of a page.
|
|
580
|
+
|
|
581
|
+
Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
|
|
405
582
|
inputSchema: z.object({
|
|
406
583
|
pageId: z.coerce.string().describe("Parent page ID (accepts string or number)"),
|
|
407
584
|
expand: z.array(z.string()).optional().describe("Additional data to expand"),
|
|
@@ -409,7 +586,11 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
409
586
|
handler: async (args) => {
|
|
410
587
|
try {
|
|
411
588
|
const result = await client.getPageChildren(args.pageId, args.expand);
|
|
412
|
-
return
|
|
589
|
+
return formatSuccessBuffered(result, {
|
|
590
|
+
resourceType: "confluence_page_children",
|
|
591
|
+
title: `Page ${args.pageId} children`,
|
|
592
|
+
pageId: args.pageId,
|
|
593
|
+
});
|
|
413
594
|
}
|
|
414
595
|
catch (error) {
|
|
415
596
|
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
@@ -433,14 +614,20 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
433
614
|
},
|
|
434
615
|
},
|
|
435
616
|
confluence_get_comments: {
|
|
436
|
-
description: `Get all comments on a Confluence page
|
|
617
|
+
description: `Get all comments on a Confluence page.
|
|
618
|
+
|
|
619
|
+
Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
|
|
437
620
|
inputSchema: z.object({
|
|
438
621
|
pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
|
|
439
622
|
}),
|
|
440
623
|
handler: async (args) => {
|
|
441
624
|
try {
|
|
442
625
|
const result = await client.getComments(args.pageId);
|
|
443
|
-
return
|
|
626
|
+
return formatSuccessBuffered(result, {
|
|
627
|
+
resourceType: "confluence_comments",
|
|
628
|
+
title: `Page ${args.pageId} comments`,
|
|
629
|
+
pageId: args.pageId,
|
|
630
|
+
});
|
|
444
631
|
}
|
|
445
632
|
catch (error) {
|
|
446
633
|
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
@@ -465,14 +652,20 @@ Use buffer_edit with element IDs to modify, then confluence_draft_create for use
|
|
|
465
652
|
},
|
|
466
653
|
},
|
|
467
654
|
confluence_list_attachments: {
|
|
468
|
-
description: `List all attachments on a Confluence page
|
|
655
|
+
description: `List all attachments on a Confluence page.
|
|
656
|
+
|
|
657
|
+
Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
|
|
469
658
|
inputSchema: z.object({
|
|
470
659
|
pageId: z.coerce.string().describe("Page ID (accepts string or number)"),
|
|
471
660
|
}),
|
|
472
661
|
handler: async (args) => {
|
|
473
662
|
try {
|
|
474
663
|
const result = await client.listAttachments(args.pageId);
|
|
475
|
-
return
|
|
664
|
+
return formatSuccessBuffered(result, {
|
|
665
|
+
resourceType: "confluence_attachments",
|
|
666
|
+
title: `Page ${args.pageId} attachments`,
|
|
667
|
+
pageId: args.pageId,
|
|
668
|
+
});
|
|
476
669
|
}
|
|
477
670
|
catch (error) {
|
|
478
671
|
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
@@ -521,44 +714,126 @@ Returns the user's personal space key and details. Use this to verify your perso
|
|
|
521
714
|
confluence_draft_create: {
|
|
522
715
|
description: `Create a Confluence draft for user review. Returns draftId, bufferId, structure (element IDs), and clickable URL.
|
|
523
716
|
|
|
524
|
-
|
|
525
|
-
|
|
717
|
+
REQUIRES bufferId - content must be in a buffer for validation and error recovery.
|
|
718
|
+
|
|
719
|
+
Workflow for NEW page:
|
|
720
|
+
1. buffer_create(content="<h1>Title</h1><p>Content</p>", contentType="xhtml") → bufferId, structure
|
|
721
|
+
2. buffer_validate_xhtml(bufferId) → check for errors, get elementId if invalid
|
|
722
|
+
3. buffer_edit(bufferId, replace=elementId, content="...") → fix errors if any
|
|
723
|
+
4. confluence_draft_create(spaceKey, title, bufferId) → creates draft for review
|
|
526
724
|
|
|
527
|
-
|
|
725
|
+
Workflow for EDITING existing page:
|
|
726
|
+
1. confluence_get_page(pageId) or confluence_edit(input) → bufferId, structure
|
|
727
|
+
2. buffer_edit(bufferId, after=ID, content/plantuml) → modify content
|
|
728
|
+
3. buffer_validate_xhtml(bufferId) → check for errors
|
|
729
|
+
4. confluence_draft_create(pageId, bufferId) → creates "[jicon-mcp REVIEW] Title" draft
|
|
730
|
+
5. User reviews in Confluence UI
|
|
731
|
+
6. confluence_review_publish(draftId) → applies changes to original page
|
|
528
732
|
|
|
529
|
-
|
|
530
|
-
1. Create draft with this tool → returns URL and structure with element IDs
|
|
531
|
-
2. User reviews draft in Confluence UI (can edit there)
|
|
532
|
-
3. If user requests changes: use confluence_draft_open to get user's edits
|
|
533
|
-
4. Modify with buffer_edit using element IDs, then confluence_draft_save for new review
|
|
534
|
-
5. User publishes manually via Confluence UI
|
|
733
|
+
On validation error: returns bufferId + errorElementId for surgical fix with buffer_edit.
|
|
535
734
|
|
|
536
|
-
|
|
735
|
+
IMPORTANT: Call help(topic="storage") for XHTML syntax (HTML vs XHTML differences).
|
|
736
|
+
IMPORTANT: Call help(topic="plantuml") for diagram syntax.`,
|
|
537
737
|
inputSchema: z.object({
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
.optional()
|
|
543
|
-
.describe("Page content in Confluence storage format (XHTML-based)"),
|
|
544
|
-
bufferId: z.string().optional().describe("Buffer ID containing content (alternative to content)"),
|
|
738
|
+
pageId: z.coerce.string().optional().describe("Existing page ID to create edit draft for. When provided, bufferId must come from that page."),
|
|
739
|
+
spaceKey: z.string().optional().describe("Space key (required for new pages, auto-populated when pageId is provided)"),
|
|
740
|
+
title: z.string().optional().describe("Page title (required for new pages, auto-populated when pageId is provided)"),
|
|
741
|
+
bufferId: z.string().describe("Buffer ID containing XHTML content (from buffer_create or confluence_get_page)"),
|
|
545
742
|
parentId: z.string().optional().describe("Parent page ID"),
|
|
546
743
|
labels: z.array(z.string()).optional().describe("Array of labels"),
|
|
547
744
|
}),
|
|
548
745
|
handler: async (args) => {
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
746
|
+
// Determine the mode: editing existing page vs creating new draft
|
|
747
|
+
let spaceKey = args.spaceKey;
|
|
748
|
+
let title = args.title;
|
|
749
|
+
let parentId = args.parentId;
|
|
750
|
+
let originalPageId;
|
|
751
|
+
let originalPageVersion;
|
|
752
|
+
// Validate buffer exists and get content
|
|
753
|
+
const bufferInfo = contentBuffer.getInfo(args.bufferId);
|
|
754
|
+
if (!bufferInfo) {
|
|
755
|
+
return formatError({
|
|
756
|
+
error: true,
|
|
757
|
+
message: `Buffer not found or expired: ${args.bufferId}`,
|
|
758
|
+
statusCode: 404,
|
|
759
|
+
details: {
|
|
760
|
+
hint: "Create a buffer first: buffer_create(content='<h1>...</h1>', contentType='xhtml')",
|
|
761
|
+
},
|
|
762
|
+
});
|
|
553
763
|
}
|
|
554
|
-
|
|
764
|
+
// Validate buffer contains XHTML content
|
|
765
|
+
if (bufferInfo.metadata?.contentType && bufferInfo.metadata.contentType !== "xhtml") {
|
|
555
766
|
return formatError({
|
|
556
767
|
error: true,
|
|
557
|
-
message:
|
|
768
|
+
message: `Buffer '${args.bufferId}' is not XHTML content (found: ${bufferInfo.metadata.contentType})`,
|
|
558
769
|
statusCode: 400,
|
|
770
|
+
details: {
|
|
771
|
+
hint: "Use buffer_create(content='...', contentType='xhtml') to create an XHTML buffer",
|
|
772
|
+
},
|
|
559
773
|
});
|
|
560
774
|
}
|
|
561
|
-
|
|
775
|
+
// Get full content from buffer
|
|
776
|
+
const fullContent = contentBuffer.getChunk(args.bufferId, 0, bufferInfo.totalSize);
|
|
777
|
+
if (!fullContent) {
|
|
778
|
+
return formatError({
|
|
779
|
+
error: true,
|
|
780
|
+
message: "Failed to retrieve buffer content",
|
|
781
|
+
statusCode: 500,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
const content = fullContent.chunk;
|
|
785
|
+
if (args.pageId) {
|
|
786
|
+
// MODE: Edit existing page - validate bufferId came from this page
|
|
787
|
+
const bufferSourceId = bufferInfo.metadata?.resourceId;
|
|
788
|
+
if (bufferSourceId !== args.pageId) {
|
|
789
|
+
return formatError({
|
|
790
|
+
error: true,
|
|
791
|
+
message: `Buffer '${args.bufferId}' does not belong to page '${args.pageId}'`,
|
|
792
|
+
statusCode: 400,
|
|
793
|
+
details: {
|
|
794
|
+
bufferSource: bufferSourceId || "unknown",
|
|
795
|
+
expectedPage: args.pageId,
|
|
796
|
+
hint: "Two options: (1) Load page first with confluence_edit(pageId) then use buffer_edit to modify, OR (2) If you already created content in a separate buffer, use buffer_edit(pageBufferId, after=N, fromBufferId=yourNewBuffer) to merge it into the page buffer",
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
// Fetch original page info to get space, title, parent, etc.
|
|
801
|
+
try {
|
|
802
|
+
const originalPage = await client.getPage(args.pageId, ["space", "ancestors", "version"]);
|
|
803
|
+
spaceKey = originalPage.space?.key;
|
|
804
|
+
title = args.title || originalPage.title; // Allow title override
|
|
805
|
+
originalPageId = originalPage.id;
|
|
806
|
+
originalPageVersion = originalPage.version?.number;
|
|
807
|
+
// Get parent from ancestors if not explicitly provided
|
|
808
|
+
if (!parentId && originalPage.ancestors && originalPage.ancestors.length > 0) {
|
|
809
|
+
parentId = originalPage.ancestors[originalPage.ancestors.length - 1].id;
|
|
810
|
+
}
|
|
811
|
+
if (!spaceKey) {
|
|
812
|
+
return formatError({
|
|
813
|
+
error: true,
|
|
814
|
+
message: `Could not determine space key for page ${args.pageId}`,
|
|
815
|
+
statusCode: 400,
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
catch (error) {
|
|
820
|
+
return formatError({
|
|
821
|
+
error: true,
|
|
822
|
+
message: `Failed to fetch original page ${args.pageId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
823
|
+
statusCode: 404,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
// MODE: New page - require spaceKey and title
|
|
829
|
+
if (!spaceKey || !title) {
|
|
830
|
+
return formatError({
|
|
831
|
+
error: true,
|
|
832
|
+
message: "For new drafts, both 'spaceKey' and 'title' are required. For editing existing pages, provide 'pageId' instead.",
|
|
833
|
+
statusCode: 400,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
}
|
|
562
837
|
// Check for raw PlantUML that should use buffer_edit with plantuml parameter
|
|
563
838
|
const rawPlantUml = detectRawPlantUml(content);
|
|
564
839
|
if (rawPlantUml) {
|
|
@@ -573,21 +848,73 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
|
|
|
573
848
|
});
|
|
574
849
|
}
|
|
575
850
|
// Validate XHTML and PlantUML content before writing
|
|
576
|
-
const validationError = await validateContentForWrite(content);
|
|
851
|
+
const validationError = await validateContentForWrite(content, args.bufferId);
|
|
577
852
|
if (validationError) {
|
|
578
853
|
return validationError;
|
|
579
854
|
}
|
|
580
855
|
// Expand PlantUML !include directives before sending to Confluence
|
|
581
856
|
const expandedContent = await expandPlantUmlInXhtml(content);
|
|
582
857
|
try {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
858
|
+
// Constants for review workflow
|
|
859
|
+
const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
|
|
860
|
+
const REVIEW_LABEL_PREFIX = 'jicon-review-';
|
|
861
|
+
let result;
|
|
862
|
+
let existingReviewDraft = null;
|
|
863
|
+
let reviewLabel;
|
|
864
|
+
if (originalPageId) {
|
|
865
|
+
// REVIEW WORKFLOW: Creating/updating review draft for existing page
|
|
866
|
+
reviewLabel = `${REVIEW_LABEL_PREFIX}${originalPageId}`;
|
|
867
|
+
// Check if review draft already exists for this page
|
|
868
|
+
existingReviewDraft = await client.findDraftByLabel(reviewLabel);
|
|
869
|
+
if (existingReviewDraft) {
|
|
870
|
+
// Delete existing review draft to update it (API doesn't support draft updates)
|
|
871
|
+
await client.deleteDraft(existingReviewDraft.id);
|
|
872
|
+
}
|
|
873
|
+
// Prevent nested review drafts
|
|
874
|
+
const reviewTitle = title.startsWith(REVIEW_PREFIX)
|
|
875
|
+
? title
|
|
876
|
+
: `${REVIEW_PREFIX}${title}`;
|
|
877
|
+
// Create review draft with label linking to original page
|
|
878
|
+
const labels = [...(args.labels || []), reviewLabel];
|
|
879
|
+
result = await client.createDraft({
|
|
880
|
+
spaceKey: spaceKey,
|
|
881
|
+
title: reviewTitle,
|
|
882
|
+
content: expandedContent,
|
|
883
|
+
parentId: parentId,
|
|
884
|
+
labels: labels,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
// NEW PAGE MODE: Create standalone draft
|
|
889
|
+
result = await client.createDraft({
|
|
890
|
+
spaceKey: spaceKey,
|
|
891
|
+
title: title,
|
|
892
|
+
content: expandedContent,
|
|
893
|
+
parentId: parentId,
|
|
894
|
+
labels: args.labels,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
// Verify draft is readable with labels (catches label attachment failures)
|
|
898
|
+
if (originalPageId && reviewLabel) {
|
|
899
|
+
const verifyDraft = await client.getDraft(result.id, [
|
|
900
|
+
...DEFAULT_PAGE_EXPAND,
|
|
901
|
+
"metadata.labels",
|
|
902
|
+
]);
|
|
903
|
+
const labels = verifyDraft.metadata?.labels?.results || [];
|
|
904
|
+
const hasReviewLabel = labels.some((l) => l.name === reviewLabel);
|
|
905
|
+
if (!hasReviewLabel) {
|
|
906
|
+
// Label attachment may have failed - try to add it again
|
|
907
|
+
// This can happen due to eventual consistency or API timing
|
|
908
|
+
try {
|
|
909
|
+
await client.addLabels(result.id, [reviewLabel], true);
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
// If label add fails, warn but continue (draft still exists)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
590
916
|
// Store content with element IDs for structured editing
|
|
917
|
+
// Track originalPageId in metadata so we can link the draft to its source
|
|
591
918
|
const { bufferId: newBufferId } = storeXhtmlWithStructure(content, {
|
|
592
919
|
resourceType: "confluence_page",
|
|
593
920
|
resourceId: result.id,
|
|
@@ -595,13 +922,16 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
|
|
|
595
922
|
spaceKey: result.space?.key,
|
|
596
923
|
title: result.title,
|
|
597
924
|
isDraft: true,
|
|
925
|
+
originalPageId: originalPageId,
|
|
926
|
+
originalPageVersion: originalPageVersion,
|
|
598
927
|
});
|
|
599
928
|
// Build the draft edit URL (drafts use resumedraft.action, not the webui link)
|
|
600
929
|
const baseUrl = client.getBaseUrl();
|
|
601
930
|
const draftUrl = `${baseUrl}/pages/resumedraft.action?draftId=${result.id}`;
|
|
602
931
|
// Generate content summary for verification
|
|
603
932
|
const summary = getContentSummary(content);
|
|
604
|
-
|
|
933
|
+
// Build response with original page info if editing existing page
|
|
934
|
+
const response = {
|
|
605
935
|
draftId: result.id,
|
|
606
936
|
bufferId: newBufferId,
|
|
607
937
|
title: result.title,
|
|
@@ -616,11 +946,37 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
|
|
|
616
946
|
hasTable: summary.hasTable,
|
|
617
947
|
headings: summary.headingCount,
|
|
618
948
|
},
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
949
|
+
};
|
|
950
|
+
// Add review workflow info when editing existing page
|
|
951
|
+
if (originalPageId) {
|
|
952
|
+
response.reviewDraft = {
|
|
953
|
+
originalPageId: originalPageId,
|
|
954
|
+
originalPageVersion: originalPageVersion,
|
|
955
|
+
reviewLabel: reviewLabel,
|
|
956
|
+
wasUpdated: !!existingReviewDraft,
|
|
957
|
+
note: "This is a REVIEW draft linked to the original page.",
|
|
958
|
+
};
|
|
959
|
+
response.message = existingReviewDraft
|
|
960
|
+
? `Review draft updated for page ${originalPageId}. Use confluence_review_publish(${result.id}) to apply changes.`
|
|
961
|
+
: `Review draft created for page ${originalPageId}. Use confluence_review_publish(${result.id}) to apply changes.`;
|
|
962
|
+
response.nextSteps = {
|
|
963
|
+
toPublish: `confluence_review_publish(reviewDraftId="${result.id}")`,
|
|
964
|
+
toDiscard: `confluence_review_discard(reviewDraftId="${result.id}")`,
|
|
965
|
+
toList: "confluence_review_list()",
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
else {
|
|
969
|
+
response.message = "Draft created. IMPORTANT: Display the URL above to the user so they can review and publish.";
|
|
970
|
+
response.afterPublish = {
|
|
971
|
+
note: "After user publishes, the draft ID becomes invalid.",
|
|
972
|
+
editAgain: `confluence_edit("${result.space?.key}/${result.title}")`,
|
|
973
|
+
tip: "Use confluence_edit with the same SPACE/Title or page URL to edit the published page.",
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (summary.plantumlCount === 0) {
|
|
977
|
+
response.warning = "No PlantUML diagrams found in content. If diagrams were expected, use buffer_edit with plantuml parameter.";
|
|
978
|
+
}
|
|
979
|
+
return formatSuccess(response);
|
|
624
980
|
}
|
|
625
981
|
catch (error) {
|
|
626
982
|
// Try to enhance XHTML errors with location info for targeted fixes
|
|
@@ -637,14 +993,16 @@ Provide either 'content' (string) OR 'bufferId' (from buffer_create/buffer_edit)
|
|
|
637
993
|
confluence_draft_open: {
|
|
638
994
|
description: `Open an existing draft page for editing. Loads content into buffer with structure (element IDs).
|
|
639
995
|
|
|
640
|
-
Use buffer_edit
|
|
996
|
+
Use buffer_edit(bufferId, after=ID, content/plantuml/fromBufferId) to modify, then confluence_draft_save. User publishes via Confluence UI.`,
|
|
641
997
|
inputSchema: z.object({
|
|
642
998
|
draftId: z.coerce.string().describe("Draft page ID"),
|
|
643
999
|
}),
|
|
644
1000
|
handler: async (args) => {
|
|
645
1001
|
try {
|
|
646
1002
|
const result = await client.getDraft(args.draftId);
|
|
647
|
-
const
|
|
1003
|
+
const rawContent = result.body?.storage?.value || "";
|
|
1004
|
+
// Collapse expanded includes back to !include directives
|
|
1005
|
+
const content = collapseExpandedIncludesInXhtml(rawContent);
|
|
648
1006
|
// Store content with element IDs for structured editing
|
|
649
1007
|
const { bufferId, structure } = storeXhtmlWithStructure(content, {
|
|
650
1008
|
resourceType: "confluence_page",
|
|
@@ -676,14 +1034,15 @@ Use buffer_edit with element IDs to modify content, then confluence_draft_save.
|
|
|
676
1034
|
},
|
|
677
1035
|
},
|
|
678
1036
|
confluence_draft_list: {
|
|
679
|
-
description: `List your draft pages. Use confluence_draft_open to load a draft for editing
|
|
1037
|
+
description: `List your draft pages. Use confluence_draft_open to load a draft for editing.
|
|
1038
|
+
|
|
1039
|
+
Returns bufferId. Use buffer_get_chunk to read, buffer_grep to search.`,
|
|
680
1040
|
inputSchema: z.object({
|
|
681
1041
|
spaceKey: z.string().optional().describe("Filter by space key"),
|
|
682
|
-
limit: z.number().optional().describe("Max results (default: 25)"),
|
|
683
1042
|
}),
|
|
684
1043
|
handler: async (args) => {
|
|
685
1044
|
try {
|
|
686
|
-
const result = await client.listUserDrafts(args.spaceKey
|
|
1045
|
+
const result = await client.listUserDrafts(args.spaceKey);
|
|
687
1046
|
// Build the base URL for constructing full URLs
|
|
688
1047
|
const baseUrl = client.getBaseUrl();
|
|
689
1048
|
const drafts = result.results.map((page) => ({
|
|
@@ -694,9 +1053,12 @@ Use buffer_edit with element IDs to modify content, then confluence_draft_save.
|
|
|
694
1053
|
created: page.version?.when || "",
|
|
695
1054
|
url: `${baseUrl}/pages/resumedraft.action?draftId=${page.id}`,
|
|
696
1055
|
}));
|
|
697
|
-
return
|
|
1056
|
+
return formatSuccessBuffered({
|
|
698
1057
|
drafts,
|
|
699
1058
|
total: result.totalSize,
|
|
1059
|
+
}, {
|
|
1060
|
+
resourceType: "confluence_drafts",
|
|
1061
|
+
title: args.spaceKey ? `Drafts in ${args.spaceKey}` : "All Drafts",
|
|
700
1062
|
});
|
|
701
1063
|
}
|
|
702
1064
|
catch (error) {
|
|
@@ -760,7 +1122,7 @@ Returns new draftId, bufferId, structure (element IDs), and URL. Always use the
|
|
|
760
1122
|
});
|
|
761
1123
|
}
|
|
762
1124
|
// Validate XHTML and PlantUML content before writing
|
|
763
|
-
const validationError = await validateContentForWrite(savedContent);
|
|
1125
|
+
const validationError = await validateContentForWrite(savedContent, args.bufferId);
|
|
764
1126
|
if (validationError) {
|
|
765
1127
|
return validationError;
|
|
766
1128
|
}
|
|
@@ -859,6 +1221,195 @@ Drafts are NOT sent to trash - they are permanently deleted.`,
|
|
|
859
1221
|
}
|
|
860
1222
|
},
|
|
861
1223
|
},
|
|
1224
|
+
// ========================================
|
|
1225
|
+
// Review Workflow Tools
|
|
1226
|
+
// ========================================
|
|
1227
|
+
// These tools manage the "[jicon-mcp REVIEW]" workflow for editing existing pages.
|
|
1228
|
+
// Review drafts are linked to original pages via labels and can be published
|
|
1229
|
+
// to apply changes to the original page.
|
|
1230
|
+
confluence_review_publish: {
|
|
1231
|
+
description: `Publish a review draft to apply changes to the original page.
|
|
1232
|
+
|
|
1233
|
+
This tool:
|
|
1234
|
+
1. Validates the draft is a "[jicon-mcp REVIEW]" draft with proper label
|
|
1235
|
+
2. Copies the draft content to the original page (creates new version)
|
|
1236
|
+
3. Deletes the review draft
|
|
1237
|
+
|
|
1238
|
+
Use this after user has reviewed the draft in Confluence UI.`,
|
|
1239
|
+
inputSchema: z.object({
|
|
1240
|
+
reviewDraftId: z.coerce.string().describe("ID of the [jicon-mcp REVIEW] draft to publish"),
|
|
1241
|
+
}),
|
|
1242
|
+
handler: async (args) => {
|
|
1243
|
+
const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
|
|
1244
|
+
const REVIEW_LABEL_PREFIX = 'jicon-review-';
|
|
1245
|
+
try {
|
|
1246
|
+
// 1. Get review draft with labels
|
|
1247
|
+
const reviewDraft = await client.getDraft(args.reviewDraftId, [
|
|
1248
|
+
...DEFAULT_PAGE_EXPAND,
|
|
1249
|
+
"metadata.labels",
|
|
1250
|
+
]);
|
|
1251
|
+
// 2. Validate it's a review draft
|
|
1252
|
+
if (!reviewDraft.title.startsWith(REVIEW_PREFIX)) {
|
|
1253
|
+
return formatError({
|
|
1254
|
+
error: true,
|
|
1255
|
+
message: `Not a jicon review draft. Title must start with "${REVIEW_PREFIX}"`,
|
|
1256
|
+
statusCode: 400,
|
|
1257
|
+
details: {
|
|
1258
|
+
actualTitle: reviewDraft.title,
|
|
1259
|
+
hint: "Use confluence_draft_create with pageId to create a review draft",
|
|
1260
|
+
},
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
// 3. Find original page from label
|
|
1264
|
+
const labels = reviewDraft.metadata?.labels?.results || [];
|
|
1265
|
+
const reviewLabel = labels.find((l) => l.name.startsWith(REVIEW_LABEL_PREFIX));
|
|
1266
|
+
if (!reviewLabel) {
|
|
1267
|
+
return formatError({
|
|
1268
|
+
error: true,
|
|
1269
|
+
message: "Review draft missing link to original page (no jicon-review-* label found)",
|
|
1270
|
+
statusCode: 400,
|
|
1271
|
+
details: {
|
|
1272
|
+
labels: labels.map((l) => l.name),
|
|
1273
|
+
hint: "This draft may have been created manually. Use confluence_draft_create with pageId to create proper review drafts.",
|
|
1274
|
+
},
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
const originalPageId = reviewLabel.name.replace(REVIEW_LABEL_PREFIX, '');
|
|
1278
|
+
// 4. Fetch original page to get current version
|
|
1279
|
+
let originalPage;
|
|
1280
|
+
try {
|
|
1281
|
+
originalPage = await client.getPage(originalPageId, ["version", "space"]);
|
|
1282
|
+
}
|
|
1283
|
+
catch (error) {
|
|
1284
|
+
return formatError({
|
|
1285
|
+
error: true,
|
|
1286
|
+
message: `Original page ${originalPageId} not found. It may have been deleted.`,
|
|
1287
|
+
statusCode: 404,
|
|
1288
|
+
details: {
|
|
1289
|
+
originalPageId,
|
|
1290
|
+
hint: "If the original page was deleted, use confluence_review_discard to remove this review draft.",
|
|
1291
|
+
},
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
// 5. Get the review draft content
|
|
1295
|
+
const reviewContent = reviewDraft.body?.storage?.value || "";
|
|
1296
|
+
// 6. Update original page with review content
|
|
1297
|
+
const updatedPage = await client.updatePage(originalPageId, originalPage.version?.number || 1, originalPage.title, // Keep original title (not the REVIEW prefix)
|
|
1298
|
+
reviewContent, false // Not a minor edit
|
|
1299
|
+
);
|
|
1300
|
+
// 7. Delete review draft
|
|
1301
|
+
await client.deleteDraft(args.reviewDraftId);
|
|
1302
|
+
// 8. Invalidate buffers
|
|
1303
|
+
contentBuffer.invalidateByMetadata({ resourceId: originalPageId });
|
|
1304
|
+
contentBuffer.invalidateByMetadata({ resourceId: args.reviewDraftId });
|
|
1305
|
+
// Build the page URL
|
|
1306
|
+
const baseUrl = client.getBaseUrl();
|
|
1307
|
+
const pageUrl = `${baseUrl}${updatedPage._links?.webui || `/pages/viewpage.action?pageId=${originalPageId}`}`;
|
|
1308
|
+
return formatSuccess({
|
|
1309
|
+
success: true,
|
|
1310
|
+
originalPageId,
|
|
1311
|
+
originalTitle: updatedPage.title,
|
|
1312
|
+
newVersion: updatedPage.version?.number,
|
|
1313
|
+
reviewDraftDeleted: args.reviewDraftId,
|
|
1314
|
+
viewUrl: pageUrl,
|
|
1315
|
+
message: `Changes from review draft applied to page "${updatedPage.title}" (version ${updatedPage.version?.number}). Review draft deleted.`,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
1320
|
+
}
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
confluence_review_discard: {
|
|
1324
|
+
description: `Discard a review draft without applying changes to the original page.
|
|
1325
|
+
|
|
1326
|
+
This tool:
|
|
1327
|
+
1. Validates the draft is a "[jicon-mcp REVIEW]" draft
|
|
1328
|
+
2. Deletes the review draft permanently
|
|
1329
|
+
3. Original page remains unchanged`,
|
|
1330
|
+
inputSchema: z.object({
|
|
1331
|
+
reviewDraftId: z.coerce.string().describe("ID of the [jicon-mcp REVIEW] draft to discard"),
|
|
1332
|
+
}),
|
|
1333
|
+
handler: async (args) => {
|
|
1334
|
+
const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
|
|
1335
|
+
try {
|
|
1336
|
+
// 1. Get review draft to validate
|
|
1337
|
+
const reviewDraft = await client.getDraft(args.reviewDraftId);
|
|
1338
|
+
// 2. Validate it's a review draft
|
|
1339
|
+
if (!reviewDraft.title.startsWith(REVIEW_PREFIX)) {
|
|
1340
|
+
return formatError({
|
|
1341
|
+
error: true,
|
|
1342
|
+
message: `Not a jicon review draft. Title must start with "${REVIEW_PREFIX}"`,
|
|
1343
|
+
statusCode: 400,
|
|
1344
|
+
details: {
|
|
1345
|
+
actualTitle: reviewDraft.title,
|
|
1346
|
+
hint: "Use confluence_draft_delete for non-review drafts",
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
// 3. Delete review draft
|
|
1351
|
+
await client.deleteDraft(args.reviewDraftId);
|
|
1352
|
+
// 4. Invalidate buffer
|
|
1353
|
+
contentBuffer.invalidateByMetadata({ resourceId: args.reviewDraftId });
|
|
1354
|
+
return formatSuccess({
|
|
1355
|
+
success: true,
|
|
1356
|
+
discarded: true,
|
|
1357
|
+
reviewDraftId: args.reviewDraftId,
|
|
1358
|
+
originalTitle: reviewDraft.title.replace(REVIEW_PREFIX, ''),
|
|
1359
|
+
message: "Review draft discarded. Original page was not modified.",
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
catch (error) {
|
|
1363
|
+
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
1364
|
+
}
|
|
1365
|
+
},
|
|
1366
|
+
},
|
|
1367
|
+
confluence_review_list: {
|
|
1368
|
+
description: `List all "[jicon-mcp REVIEW]" drafts for cleanup or management.
|
|
1369
|
+
|
|
1370
|
+
Returns review drafts with their linked original page IDs.
|
|
1371
|
+
Use this to find abandoned review drafts or manage multiple review workflows.`,
|
|
1372
|
+
inputSchema: z.object({
|
|
1373
|
+
spaceKey: z.string().optional().describe("Filter by space key (optional)"),
|
|
1374
|
+
}),
|
|
1375
|
+
handler: async (args) => {
|
|
1376
|
+
const REVIEW_PREFIX = '[jicon-mcp REVIEW] ';
|
|
1377
|
+
const REVIEW_LABEL_PREFIX = 'jicon-review-';
|
|
1378
|
+
try {
|
|
1379
|
+
// 1. List user's drafts with labels expanded
|
|
1380
|
+
const drafts = await client.listUserDrafts(args.spaceKey, 500);
|
|
1381
|
+
// 2. Filter to review drafts only
|
|
1382
|
+
const reviewDrafts = drafts.results.filter((draft) => draft.title?.startsWith(REVIEW_PREFIX));
|
|
1383
|
+
// 3. Build response with original page info from labels
|
|
1384
|
+
const baseUrl = client.getBaseUrl();
|
|
1385
|
+
const reviews = reviewDrafts.map((draft) => {
|
|
1386
|
+
const labels = draft.metadata?.labels?.results || [];
|
|
1387
|
+
const reviewLabel = labels.find((l) => l.name.startsWith(REVIEW_LABEL_PREFIX));
|
|
1388
|
+
const originalPageId = reviewLabel?.name.replace(REVIEW_LABEL_PREFIX, '') || 'unknown';
|
|
1389
|
+
return {
|
|
1390
|
+
reviewDraftId: draft.id,
|
|
1391
|
+
title: draft.title,
|
|
1392
|
+
originalTitle: draft.title.replace(REVIEW_PREFIX, ''),
|
|
1393
|
+
originalPageId,
|
|
1394
|
+
spaceKey: draft.space?.key || '',
|
|
1395
|
+
spaceName: draft.space?.name || '',
|
|
1396
|
+
createdDate: draft.version?.when || '',
|
|
1397
|
+
editUrl: `${baseUrl}/pages/resumedraft.action?draftId=${draft.id}`,
|
|
1398
|
+
};
|
|
1399
|
+
});
|
|
1400
|
+
return formatSuccess({
|
|
1401
|
+
reviewDrafts: reviews,
|
|
1402
|
+
total: reviews.length,
|
|
1403
|
+
message: reviews.length > 0
|
|
1404
|
+
? `Found ${reviews.length} review draft(s). Use confluence_review_publish or confluence_review_discard to manage.`
|
|
1405
|
+
: "No review drafts found.",
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
catch (error) {
|
|
1409
|
+
return formatError(isApiError(error) ? error : new Error(String(error)));
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1412
|
+
},
|
|
862
1413
|
};
|
|
863
1414
|
}
|
|
864
1415
|
//# sourceMappingURL=tools.js.map
|