@de-otio/epimethian-mcp 6.0.1 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -1
- package/dist/cli/index.js +676 -79
- package/dist/cli/index.js.map +4 -4
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -28993,6 +28993,75 @@ var init_page_cache = __esm({
|
|
|
28993
28993
|
}
|
|
28994
28994
|
});
|
|
28995
28995
|
|
|
28996
|
+
// src/server/config.ts
|
|
28997
|
+
function resolvePosture(settings) {
|
|
28998
|
+
if (settings?.posture) return settings.posture;
|
|
28999
|
+
if (settings?.readOnly === true) return "read-only";
|
|
29000
|
+
if (settings?.readOnly === false) return "read-write";
|
|
29001
|
+
const envVal = process.env.CONFLUENCE_READ_ONLY;
|
|
29002
|
+
if (envVal === "true") return "read-only";
|
|
29003
|
+
if (envVal === "false") return "read-write";
|
|
29004
|
+
return "detect";
|
|
29005
|
+
}
|
|
29006
|
+
function resolveUnverifiedStatusFlag(settings) {
|
|
29007
|
+
if (settings?.unverifiedStatus !== void 0) return settings.unverifiedStatus;
|
|
29008
|
+
if (process.env.CONFLUENCE_UNVERIFIED_STATUS === "false") return false;
|
|
29009
|
+
if (process.env.CONFLUENCE_UNVERIFIED_STATUS === "true") return true;
|
|
29010
|
+
return true;
|
|
29011
|
+
}
|
|
29012
|
+
function resolveEffectivePosture(configured, probed) {
|
|
29013
|
+
if (configured === "read-only") {
|
|
29014
|
+
return { effective: "read-only", source: "profile" };
|
|
29015
|
+
}
|
|
29016
|
+
if (configured === "read-write") {
|
|
29017
|
+
if (probed === "read-only") {
|
|
29018
|
+
return {
|
|
29019
|
+
effective: "read-write",
|
|
29020
|
+
source: "profile",
|
|
29021
|
+
warning: "configured read-write but probe indicates token is read-only; writes will likely fail"
|
|
29022
|
+
};
|
|
29023
|
+
}
|
|
29024
|
+
return { effective: "read-write", source: "profile" };
|
|
29025
|
+
}
|
|
29026
|
+
if (probed === "write") {
|
|
29027
|
+
return { effective: "read-write", source: "probe" };
|
|
29028
|
+
}
|
|
29029
|
+
if (probed === "read-only") {
|
|
29030
|
+
return { effective: "read-only", source: "probe" };
|
|
29031
|
+
}
|
|
29032
|
+
return {
|
|
29033
|
+
effective: "read-write",
|
|
29034
|
+
source: "default",
|
|
29035
|
+
warning: "capability probe was inconclusive \u2014 defaulting to read-write"
|
|
29036
|
+
};
|
|
29037
|
+
}
|
|
29038
|
+
function resolveUnverifiedStatusLocale(settings) {
|
|
29039
|
+
if (settings?.unverifiedStatusLocale) return settings.unverifiedStatusLocale;
|
|
29040
|
+
if (process.env.CONFLUENCE_UNVERIFIED_STATUS_LOCALE) {
|
|
29041
|
+
return process.env.CONFLUENCE_UNVERIFIED_STATUS_LOCALE;
|
|
29042
|
+
}
|
|
29043
|
+
return void 0;
|
|
29044
|
+
}
|
|
29045
|
+
var ProfileSettingsValidator;
|
|
29046
|
+
var init_config = __esm({
|
|
29047
|
+
"src/server/config.ts"() {
|
|
29048
|
+
"use strict";
|
|
29049
|
+
init_zod();
|
|
29050
|
+
ProfileSettingsValidator = external_exports.object({
|
|
29051
|
+
readOnly: external_exports.boolean().optional(),
|
|
29052
|
+
posture: external_exports.enum(["read-only", "read-write", "detect"]).optional(),
|
|
29053
|
+
attribution: external_exports.boolean().optional(),
|
|
29054
|
+
unverifiedStatus: external_exports.boolean().optional(),
|
|
29055
|
+
unverifiedStatusLocale: external_exports.string().optional(),
|
|
29056
|
+
unverifiedStatusName: external_exports.string().max(20).optional(),
|
|
29057
|
+
unverifiedStatusColor: external_exports.enum(["#FFC400", "#2684FF", "#57D9A3", "#FF7452", "#8777D9"]).optional(),
|
|
29058
|
+
allowed_tools: external_exports.array(external_exports.string()).optional(),
|
|
29059
|
+
denied_tools: external_exports.array(external_exports.string()).optional(),
|
|
29060
|
+
spaces: external_exports.array(external_exports.string()).optional()
|
|
29061
|
+
}).strict();
|
|
29062
|
+
}
|
|
29063
|
+
});
|
|
29064
|
+
|
|
28996
29065
|
// node_modules/he/he.js
|
|
28997
29066
|
var require_he = __commonJS({
|
|
28998
29067
|
"node_modules/he/he.js"(exports2, module2) {
|
|
@@ -34945,6 +35014,67 @@ var require_dist2 = __commonJS({
|
|
|
34945
35014
|
});
|
|
34946
35015
|
|
|
34947
35016
|
// src/server/confluence-client.ts
|
|
35017
|
+
var confluence_client_exports = {};
|
|
35018
|
+
__export(confluence_client_exports, {
|
|
35019
|
+
CommentSchema: () => CommentSchema,
|
|
35020
|
+
ConfluenceApiError: () => ConfluenceApiError,
|
|
35021
|
+
ConfluenceAuthError: () => ConfluenceAuthError,
|
|
35022
|
+
ConfluenceConflictError: () => ConfluenceConflictError,
|
|
35023
|
+
ConfluenceNotFoundError: () => ConfluenceNotFoundError,
|
|
35024
|
+
ConfluencePermissionError: () => ConfluencePermissionError,
|
|
35025
|
+
ContentStateSchema: () => ContentStateSchema,
|
|
35026
|
+
LabelSchema: () => LabelSchema,
|
|
35027
|
+
PageSchema: () => PageSchema,
|
|
35028
|
+
ProfileNotConfiguredError: () => ProfileNotConfiguredError,
|
|
35029
|
+
_rawCreatePage: () => _rawCreatePage,
|
|
35030
|
+
_rawUpdatePage: () => _rawUpdatePage,
|
|
35031
|
+
addLabels: () => addLabels,
|
|
35032
|
+
createFooterComment: () => createFooterComment,
|
|
35033
|
+
createInlineComment: () => createInlineComment,
|
|
35034
|
+
deleteFooterComment: () => deleteFooterComment,
|
|
35035
|
+
deleteInlineComment: () => deleteInlineComment,
|
|
35036
|
+
deletePage: () => deletePage,
|
|
35037
|
+
ensureAttributionLabel: () => ensureAttributionLabel,
|
|
35038
|
+
extractHeadings: () => extractHeadings,
|
|
35039
|
+
extractSection: () => extractSection,
|
|
35040
|
+
extractSectionBody: () => extractSectionBody,
|
|
35041
|
+
formatPage: () => formatPage,
|
|
35042
|
+
getAttachments: () => getAttachments,
|
|
35043
|
+
getCommentReplies: () => getCommentReplies,
|
|
35044
|
+
getConfig: () => getConfig,
|
|
35045
|
+
getContentState: () => getContentState,
|
|
35046
|
+
getFooterComments: () => getFooterComments,
|
|
35047
|
+
getInlineComments: () => getInlineComments,
|
|
35048
|
+
getLabels: () => getLabels,
|
|
35049
|
+
getPage: () => getPage,
|
|
35050
|
+
getPageByTitle: () => getPageByTitle,
|
|
35051
|
+
getPageChildren: () => getPageChildren,
|
|
35052
|
+
getPageVersionBody: () => getPageVersionBody,
|
|
35053
|
+
getPageVersions: () => getPageVersions,
|
|
35054
|
+
getSpaces: () => getSpaces,
|
|
35055
|
+
listPages: () => listPages,
|
|
35056
|
+
looksLikeMarkdown: () => looksLikeMarkdown,
|
|
35057
|
+
normalizeBodyForSubmit: () => normalizeBodyForSubmit,
|
|
35058
|
+
probeWriteCapability: () => probeWriteCapability,
|
|
35059
|
+
removeContentState: () => removeContentState,
|
|
35060
|
+
removeLabel: () => removeLabel,
|
|
35061
|
+
replaceSection: () => replaceSection,
|
|
35062
|
+
resolveComment: () => resolveComment,
|
|
35063
|
+
resolveCredentials: () => resolveCredentials,
|
|
35064
|
+
resolveSpaceId: () => resolveSpaceId,
|
|
35065
|
+
sanitizeCommentBody: () => sanitizeCommentBody,
|
|
35066
|
+
sanitizeError: () => sanitizeError,
|
|
35067
|
+
searchPages: () => searchPages,
|
|
35068
|
+
searchPagesByTitle: () => searchPagesByTitle,
|
|
35069
|
+
searchUsers: () => searchUsers,
|
|
35070
|
+
setClientLabel: () => setClientLabel,
|
|
35071
|
+
setContentState: () => setContentState,
|
|
35072
|
+
toMarkdownView: () => toMarkdownView,
|
|
35073
|
+
toStorageFormat: () => toStorageFormat,
|
|
35074
|
+
truncateStorageFormat: () => truncateStorageFormat,
|
|
35075
|
+
uploadAttachment: () => uploadAttachment,
|
|
35076
|
+
validateStartup: () => validateStartup
|
|
35077
|
+
});
|
|
34948
35078
|
function setClientLabel(label) {
|
|
34949
35079
|
if (!label) {
|
|
34950
35080
|
_clientLabel = void 0;
|
|
@@ -35004,8 +35134,13 @@ async function getConfig() {
|
|
|
35004
35134
|
if (_config) return _config;
|
|
35005
35135
|
const { url, email: email2, apiToken, profile, sealedCloudId, sealedDisplayName } = await resolveCredentials();
|
|
35006
35136
|
const registrySettings = profile ? await getProfileSettings(profile) : void 0;
|
|
35007
|
-
const
|
|
35137
|
+
const posture = resolvePosture(registrySettings);
|
|
35138
|
+
const readOnly = posture === "read-only";
|
|
35008
35139
|
const attribution = registrySettings?.attribution !== false && process.env.CONFLUENCE_ATTRIBUTION !== "false";
|
|
35140
|
+
const unverifiedStatus = resolveUnverifiedStatusFlag(registrySettings);
|
|
35141
|
+
const unverifiedStatusLocale = resolveUnverifiedStatusLocale(registrySettings);
|
|
35142
|
+
const unverifiedStatusName = registrySettings?.unverifiedStatusName;
|
|
35143
|
+
const unverifiedStatusColor = registrySettings?.unverifiedStatusColor;
|
|
35009
35144
|
const authHeader = "Basic " + Buffer.from(`${email2}:${apiToken}`).toString("base64");
|
|
35010
35145
|
const jsonHeaders = Object.freeze({
|
|
35011
35146
|
Authorization: authHeader,
|
|
@@ -35016,16 +35151,83 @@ async function getConfig() {
|
|
|
35016
35151
|
email: email2,
|
|
35017
35152
|
profile,
|
|
35018
35153
|
readOnly,
|
|
35154
|
+
posture,
|
|
35019
35155
|
attribution,
|
|
35156
|
+
unverifiedStatus,
|
|
35157
|
+
unverifiedStatusLocale,
|
|
35158
|
+
unverifiedStatusName,
|
|
35159
|
+
unverifiedStatusColor,
|
|
35020
35160
|
apiV2: `${url}/wiki/api/v2`,
|
|
35021
35161
|
apiV1: `${url}/wiki/rest/api`,
|
|
35022
35162
|
authHeader,
|
|
35023
35163
|
jsonHeaders,
|
|
35024
35164
|
sealedCloudId,
|
|
35025
|
-
sealedDisplayName
|
|
35165
|
+
sealedDisplayName,
|
|
35166
|
+
// Placeholders — overwritten by validateStartup() once the probe runs.
|
|
35167
|
+
// Until then, treat as read-write to preserve existing behavior.
|
|
35168
|
+
effectivePosture: posture === "read-only" ? "read-only" : "read-write",
|
|
35169
|
+
probedCapability: null,
|
|
35170
|
+
postureSource: "default"
|
|
35026
35171
|
});
|
|
35027
35172
|
return _config;
|
|
35028
35173
|
}
|
|
35174
|
+
async function probeWriteCapability() {
|
|
35175
|
+
const cfg = await getConfig();
|
|
35176
|
+
try {
|
|
35177
|
+
const spaces = await getSpaces(1);
|
|
35178
|
+
if (spaces.length > 0) {
|
|
35179
|
+
const spaceKey = spaces[0].key;
|
|
35180
|
+
const permUrl = new URL(
|
|
35181
|
+
`${cfg.apiV1}/user/current/permission/space/${encodeURIComponent(spaceKey)}`
|
|
35182
|
+
);
|
|
35183
|
+
permUrl.searchParams.set("operationKey", "create");
|
|
35184
|
+
permUrl.searchParams.set("targetType", "page");
|
|
35185
|
+
try {
|
|
35186
|
+
const res = await confluenceRequest(permUrl.toString());
|
|
35187
|
+
const raw = await res.json();
|
|
35188
|
+
if (raw && typeof raw === "object") {
|
|
35189
|
+
const obj = raw;
|
|
35190
|
+
if (obj.havePermission === true) return "write";
|
|
35191
|
+
if (obj.havePermission === false) return "read-only";
|
|
35192
|
+
if (Array.isArray(obj.permissions)) {
|
|
35193
|
+
const perms = obj.permissions;
|
|
35194
|
+
const hasCreate = perms.some((p) => {
|
|
35195
|
+
const op = p.operation;
|
|
35196
|
+
return op?.operation === "create" && op?.targetType === "page";
|
|
35197
|
+
});
|
|
35198
|
+
return hasCreate ? "write" : "read-only";
|
|
35199
|
+
}
|
|
35200
|
+
}
|
|
35201
|
+
} catch (permErr) {
|
|
35202
|
+
if (permErr instanceof ConfluenceAuthError) throw permErr;
|
|
35203
|
+
if (permErr instanceof ConfluenceNotFoundError) {
|
|
35204
|
+
} else if (permErr instanceof ConfluencePermissionError) {
|
|
35205
|
+
return "read-only";
|
|
35206
|
+
}
|
|
35207
|
+
}
|
|
35208
|
+
}
|
|
35209
|
+
} catch (spacesErr) {
|
|
35210
|
+
if (spacesErr instanceof ConfluenceAuthError) throw spacesErr;
|
|
35211
|
+
}
|
|
35212
|
+
try {
|
|
35213
|
+
await confluenceRequest(`${cfg.apiV2}/pages/999999999999`, {
|
|
35214
|
+
method: "PUT",
|
|
35215
|
+
body: JSON.stringify({})
|
|
35216
|
+
});
|
|
35217
|
+
return "write";
|
|
35218
|
+
} catch (err) {
|
|
35219
|
+
if (err instanceof ConfluenceAuthError) throw err;
|
|
35220
|
+
if (err instanceof ConfluencePermissionError) return "read-only";
|
|
35221
|
+
if (err instanceof ConfluenceNotFoundError) return "write";
|
|
35222
|
+
if (err instanceof ConfluenceApiError && err.status >= 400 && err.status < 500) {
|
|
35223
|
+
return "write";
|
|
35224
|
+
}
|
|
35225
|
+
console.error(
|
|
35226
|
+
`epimethian-mcp: warning \u2014 write-capability probe failed with unexpected error: ${err instanceof Error ? err.message : String(err)}`
|
|
35227
|
+
);
|
|
35228
|
+
return "inconclusive";
|
|
35229
|
+
}
|
|
35230
|
+
}
|
|
35029
35231
|
async function validateStartup(config3) {
|
|
35030
35232
|
const { url, email: email2, profile } = config3;
|
|
35031
35233
|
const decoded = Buffer.from(
|
|
@@ -35058,12 +35260,38 @@ Expected user: ${email2}
|
|
|
35058
35260
|
if (profile) {
|
|
35059
35261
|
await verifyOrSealTenant(config3, apiToken);
|
|
35060
35262
|
}
|
|
35263
|
+
const configuredPosture = config3.posture ?? "detect";
|
|
35264
|
+
let probedCapability = null;
|
|
35265
|
+
if (configuredPosture === "detect") {
|
|
35266
|
+
try {
|
|
35267
|
+
probedCapability = await probeWriteCapability();
|
|
35268
|
+
} catch (err) {
|
|
35269
|
+
console.error(
|
|
35270
|
+
`epimethian-mcp: warning \u2014 write-capability probe threw unexpectedly: ${err instanceof Error ? err.message : String(err)}`
|
|
35271
|
+
);
|
|
35272
|
+
probedCapability = "inconclusive";
|
|
35273
|
+
}
|
|
35274
|
+
}
|
|
35275
|
+
const resolved = resolveEffectivePosture(configuredPosture, probedCapability);
|
|
35276
|
+
_config = Object.freeze({
|
|
35277
|
+
..._config,
|
|
35278
|
+
effectivePosture: resolved.effective,
|
|
35279
|
+
probedCapability,
|
|
35280
|
+
postureSource: resolved.source
|
|
35281
|
+
});
|
|
35061
35282
|
const profileLabel = profile ? `profile: ${profile}` : "env-var mode";
|
|
35062
35283
|
const readOnlyLabel = config3.readOnly ? ", READ-ONLY" : "";
|
|
35063
35284
|
const attributionLabel = config3.attribution ? "" : ", NO-ATTRIBUTION";
|
|
35064
35285
|
console.error(
|
|
35065
35286
|
`epimethian-mcp: connected to ${url} as ${email2} (${profileLabel}${readOnlyLabel}${attributionLabel})`
|
|
35066
35287
|
);
|
|
35288
|
+
const modeLabel = resolved.effective === "read-only" ? "read-only" : "read-write";
|
|
35289
|
+
console.error(
|
|
35290
|
+
`[epimethian-mcp] Profile "${profile ?? "env-var"}" \u2014 mode: ${modeLabel} (source: ${resolved.source}).`
|
|
35291
|
+
);
|
|
35292
|
+
if (resolved.warning) {
|
|
35293
|
+
console.error(`[epimethian-mcp] Warning: ${resolved.warning}`);
|
|
35294
|
+
}
|
|
35067
35295
|
}
|
|
35068
35296
|
async function verifyOrSealTenant(config3, apiToken) {
|
|
35069
35297
|
const { url, email: email2, profile, sealedCloudId, sealedDisplayName } = config3;
|
|
@@ -35138,7 +35366,15 @@ async function confluenceRequest(url, options2 = {}) {
|
|
|
35138
35366
|
if (!res.ok) {
|
|
35139
35367
|
const body = await res.text();
|
|
35140
35368
|
console.error(`Confluence API error (${res.status}): ${sanitizeError(body)}`);
|
|
35141
|
-
|
|
35369
|
+
if (res.status === 401) {
|
|
35370
|
+
throw new ConfluenceAuthError(res.status, body);
|
|
35371
|
+
} else if (res.status === 403) {
|
|
35372
|
+
throw new ConfluencePermissionError(res.status, body);
|
|
35373
|
+
} else if (res.status === 404) {
|
|
35374
|
+
throw new ConfluenceNotFoundError(res.status, body);
|
|
35375
|
+
} else {
|
|
35376
|
+
throw new ConfluenceApiError(res.status, body);
|
|
35377
|
+
}
|
|
35142
35378
|
}
|
|
35143
35379
|
return res;
|
|
35144
35380
|
}
|
|
@@ -35206,7 +35442,7 @@ async function getPage(pageId, includeBody) {
|
|
|
35206
35442
|
async function _rawCreatePage(spaceId, title, body, parentId, clientLabel) {
|
|
35207
35443
|
const cfg = await getConfig();
|
|
35208
35444
|
const pageBody = normalizeBodyForSubmit(body);
|
|
35209
|
-
const epimethianTag = `Epimethian v${"6.0
|
|
35445
|
+
const epimethianTag = `Epimethian v${"6.1.0"}`;
|
|
35210
35446
|
const versionMsg = cfg.attribution && clientLabel ? `Created by ${clientLabel} (via ${epimethianTag})` : `Created by ${epimethianTag}`;
|
|
35211
35447
|
const payload = {
|
|
35212
35448
|
title,
|
|
@@ -35222,16 +35458,12 @@ async function _rawCreatePage(spaceId, title, body, parentId, clientLabel) {
|
|
|
35222
35458
|
const raw = await v2Post("/pages", payload);
|
|
35223
35459
|
const page = PageSchema.parse(raw);
|
|
35224
35460
|
pageCache.set(page.id, page.version?.number ?? 1, pageBody);
|
|
35225
|
-
try {
|
|
35226
|
-
await ensureAttributionLabel(page.id);
|
|
35227
|
-
} catch {
|
|
35228
|
-
}
|
|
35229
35461
|
return page;
|
|
35230
35462
|
}
|
|
35231
35463
|
async function _rawUpdatePage(pageId, opts) {
|
|
35232
35464
|
const cfg = await getConfig();
|
|
35233
35465
|
const newVersion = opts.version + 1;
|
|
35234
|
-
const epimethianTag = `Epimethian v${"6.0
|
|
35466
|
+
const epimethianTag = `Epimethian v${"6.1.0"}`;
|
|
35235
35467
|
const effectiveClient = cfg.attribution ? opts.clientLabel : void 0;
|
|
35236
35468
|
let versionMessage;
|
|
35237
35469
|
if (opts.versionMessage && effectiveClient)
|
|
@@ -35276,10 +35508,6 @@ async function _rawUpdatePage(pageId, opts) {
|
|
|
35276
35508
|
if (pageBody !== void 0) {
|
|
35277
35509
|
pageCache.set(pageId, newVersion, pageBody);
|
|
35278
35510
|
}
|
|
35279
|
-
try {
|
|
35280
|
-
await ensureAttributionLabel(page.id);
|
|
35281
|
-
} catch {
|
|
35282
|
-
}
|
|
35283
35511
|
return { page, newVersion };
|
|
35284
35512
|
}
|
|
35285
35513
|
async function deletePage(pageId, expectedVersion) {
|
|
@@ -35448,10 +35676,20 @@ async function uploadAttachment(pageId, fileData, filename, comment2) {
|
|
|
35448
35676
|
return { title: att.title, id: att.id, fileSize: att.extensions?.fileSize };
|
|
35449
35677
|
}
|
|
35450
35678
|
async function ensureAttributionLabel(pageId) {
|
|
35451
|
-
|
|
35452
|
-
|
|
35453
|
-
|
|
35454
|
-
|
|
35679
|
+
try {
|
|
35680
|
+
await addLabels(pageId, [ATTRIBUTION_LABEL]);
|
|
35681
|
+
const labels = await getLabels(pageId);
|
|
35682
|
+
if (labels.some((l) => l.name === LEGACY_ATTRIBUTION_LABEL)) {
|
|
35683
|
+
await removeLabel(pageId, LEGACY_ATTRIBUTION_LABEL);
|
|
35684
|
+
}
|
|
35685
|
+
return {};
|
|
35686
|
+
} catch (err) {
|
|
35687
|
+
if (err instanceof ConfluencePermissionError) {
|
|
35688
|
+
return {
|
|
35689
|
+
warning: `Could not apply 'epimethian-edited' label (permission denied). Provenance label is missing for page ${pageId}.`
|
|
35690
|
+
};
|
|
35691
|
+
}
|
|
35692
|
+
throw err;
|
|
35455
35693
|
}
|
|
35456
35694
|
}
|
|
35457
35695
|
function stripAttributionFooter(body) {
|
|
@@ -35908,7 +36146,7 @@ async function formatPage(page, optionsOrIncludeBody) {
|
|
|
35908
36146
|
}
|
|
35909
36147
|
return lines.join("\n");
|
|
35910
36148
|
}
|
|
35911
|
-
var import_turndown, CLIENT_LABEL_DISALLOWED_RE, _clientLabel, _config, ProfileNotConfiguredError, PageSchema, PagesResultSchema, SpaceSchema, SpacesResultSchema, AttachmentSchema, AttachmentsResultSchema, LabelSchema, LabelsResultSchema, ContentStateSchema, CommentSchema, CommentsResultSchema, UploadResultSchema, VersionMetadataSchema, VersionsResultSchema, V1PageVersionSchema, ConfluenceApiError, ConfluenceConflictError, UserSchema, UserSearchResultSchema, ATTRIBUTION_LABEL, LEGACY_ATTRIBUTION_LABEL, DANGEROUS_TAG_RE, HTML_TAG_RE, HTML_ENTITY_RE, SAFE_MACRO_PARAMS;
|
|
36149
|
+
var import_turndown, CLIENT_LABEL_DISALLOWED_RE, _clientLabel, _config, ProfileNotConfiguredError, PageSchema, PagesResultSchema, SpaceSchema, SpacesResultSchema, AttachmentSchema, AttachmentsResultSchema, LabelSchema, LabelsResultSchema, ContentStateSchema, CommentSchema, CommentsResultSchema, UploadResultSchema, VersionMetadataSchema, VersionsResultSchema, V1PageVersionSchema, ConfluenceApiError, ConfluenceAuthError, ConfluencePermissionError, ConfluenceNotFoundError, ConfluenceConflictError, UserSchema, UserSearchResultSchema, ATTRIBUTION_LABEL, LEGACY_ATTRIBUTION_LABEL, DANGEROUS_TAG_RE, HTML_TAG_RE, HTML_ENTITY_RE, SAFE_MACRO_PARAMS;
|
|
35912
36150
|
var init_confluence_client = __esm({
|
|
35913
36151
|
"src/server/confluence-client.ts"() {
|
|
35914
36152
|
"use strict";
|
|
@@ -35920,6 +36158,7 @@ var init_confluence_client = __esm({
|
|
|
35920
36158
|
init_escape();
|
|
35921
36159
|
init_untrusted_fence();
|
|
35922
36160
|
init_page_cache();
|
|
36161
|
+
init_config();
|
|
35923
36162
|
CLIENT_LABEL_DISALLOWED_RE = /[^A-Za-z0-9 _./()\-]/g;
|
|
35924
36163
|
_config = null;
|
|
35925
36164
|
ProfileNotConfiguredError = class extends Error {
|
|
@@ -36039,6 +36278,12 @@ var init_confluence_client = __esm({
|
|
|
36039
36278
|
this.status = status;
|
|
36040
36279
|
}
|
|
36041
36280
|
};
|
|
36281
|
+
ConfluenceAuthError = class extends ConfluenceApiError {
|
|
36282
|
+
};
|
|
36283
|
+
ConfluencePermissionError = class extends ConfluenceApiError {
|
|
36284
|
+
};
|
|
36285
|
+
ConfluenceNotFoundError = class extends ConfluenceApiError {
|
|
36286
|
+
};
|
|
36042
36287
|
ConfluenceConflictError = class extends Error {
|
|
36043
36288
|
constructor(pageId) {
|
|
36044
36289
|
super(
|
|
@@ -47375,6 +47620,62 @@ var init_safe_write = __esm({
|
|
|
47375
47620
|
}
|
|
47376
47621
|
});
|
|
47377
47622
|
|
|
47623
|
+
// src/server/check-permissions.ts
|
|
47624
|
+
var check_permissions_exports = {};
|
|
47625
|
+
__export(check_permissions_exports, {
|
|
47626
|
+
buildCheckPermissionsPayload: () => buildCheckPermissionsPayload
|
|
47627
|
+
});
|
|
47628
|
+
function buildCheckPermissionsPayload(config3) {
|
|
47629
|
+
const effective = config3.effectivePosture ?? "read-write";
|
|
47630
|
+
const configured = config3.posture ?? config3.effectivePosture ?? "read-write";
|
|
47631
|
+
const source = config3.postureSource ?? "default";
|
|
47632
|
+
let writePages;
|
|
47633
|
+
if (config3.probedCapability === "write") {
|
|
47634
|
+
writePages = true;
|
|
47635
|
+
} else if (config3.probedCapability === "read-only") {
|
|
47636
|
+
writePages = false;
|
|
47637
|
+
} else {
|
|
47638
|
+
writePages = "unknown";
|
|
47639
|
+
}
|
|
47640
|
+
const notes = [];
|
|
47641
|
+
if (effective === "read-only" && writePages === true) {
|
|
47642
|
+
notes.push(
|
|
47643
|
+
"This profile is pinned to read-only mode by user configuration. The underlying token has write access, but write tools are not exposed to the agent."
|
|
47644
|
+
);
|
|
47645
|
+
} else if (effective === "read-only" && writePages === false) {
|
|
47646
|
+
notes.push("Both the profile and the token are read-only. Write tools are not exposed.");
|
|
47647
|
+
} else if (effective === "read-write" && writePages === false) {
|
|
47648
|
+
notes.push(
|
|
47649
|
+
"WARNING: this profile is configured read-write but the token does not appear to have write access. Writes will likely fail."
|
|
47650
|
+
);
|
|
47651
|
+
}
|
|
47652
|
+
return {
|
|
47653
|
+
profile: config3.profile,
|
|
47654
|
+
user: { email: config3.email },
|
|
47655
|
+
posture: {
|
|
47656
|
+
effective,
|
|
47657
|
+
configured,
|
|
47658
|
+
source
|
|
47659
|
+
},
|
|
47660
|
+
tokenCapability: {
|
|
47661
|
+
authenticated: true,
|
|
47662
|
+
listSpaces: true,
|
|
47663
|
+
readPages: true,
|
|
47664
|
+
writePages,
|
|
47665
|
+
addLabels: "unknown",
|
|
47666
|
+
setContentState: "unknown",
|
|
47667
|
+
addAttachments: "unknown",
|
|
47668
|
+
addComments: "unknown"
|
|
47669
|
+
},
|
|
47670
|
+
notes
|
|
47671
|
+
};
|
|
47672
|
+
}
|
|
47673
|
+
var init_check_permissions = __esm({
|
|
47674
|
+
"src/server/check-permissions.ts"() {
|
|
47675
|
+
"use strict";
|
|
47676
|
+
}
|
|
47677
|
+
});
|
|
47678
|
+
|
|
47378
47679
|
// src/shared/update-check.ts
|
|
47379
47680
|
function parseSemVer(version2) {
|
|
47380
47681
|
const match2 = version2.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
@@ -47727,23 +48028,39 @@ Profile will be saved without a tenant seal. Cross-tenant verification at startu
|
|
|
47727
48028
|
);
|
|
47728
48029
|
if (profile) {
|
|
47729
48030
|
await addToProfileRegistry(profile);
|
|
47730
|
-
const
|
|
47731
|
-
|
|
47732
|
-
|
|
47733
|
-
|
|
47734
|
-
|
|
47735
|
-
|
|
47736
|
-
|
|
47737
|
-
|
|
47738
|
-
|
|
47739
|
-
|
|
48031
|
+
const rl2 = readline.createInterface({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
48032
|
+
try {
|
|
48033
|
+
let posture = "read-only";
|
|
48034
|
+
let validInput = false;
|
|
48035
|
+
while (!validInput) {
|
|
48036
|
+
const answer = (await rl2.question(
|
|
48037
|
+
`MCP access mode for this profile:
|
|
48038
|
+
[1] Read-only \u2014 the agent cannot modify Confluence through this profile,
|
|
48039
|
+
regardless of what the API token can do. Recommended for
|
|
48040
|
+
untrusted agents, exploratory work, and defense-in-depth.
|
|
48041
|
+
[2] Read-write \u2014 the agent can create, update, and delete pages.
|
|
48042
|
+
[3] Detect at startup \u2014 infer from the token's actual permissions.
|
|
48043
|
+
Your choice [default: 1]: `
|
|
48044
|
+
)).trim();
|
|
48045
|
+
if (answer === "" || answer === "1") {
|
|
48046
|
+
posture = "read-only";
|
|
48047
|
+
validInput = true;
|
|
48048
|
+
} else if (answer === "2") {
|
|
48049
|
+
posture = "read-write";
|
|
48050
|
+
validInput = true;
|
|
48051
|
+
} else if (answer === "3") {
|
|
48052
|
+
posture = "detect";
|
|
48053
|
+
validInput = true;
|
|
48054
|
+
} else {
|
|
48055
|
+
console.log(`Invalid choice "${answer}". Please enter 1, 2, or 3.`);
|
|
48056
|
+
}
|
|
47740
48057
|
}
|
|
47741
|
-
|
|
47742
|
-
|
|
47743
|
-
await setProfileSettings(profile, { readOnly });
|
|
47744
|
-
const modeLabel = readOnly ? "read-only" : "read-write";
|
|
47745
|
-
console.log(`Credentials saved to OS keychain (profile: ${profile}, ${modeLabel}).
|
|
48058
|
+
await setProfileSettings(profile, { posture });
|
|
48059
|
+
console.log(`Credentials saved to OS keychain (profile: ${profile}, posture: ${posture}).
|
|
47746
48060
|
`);
|
|
48061
|
+
} finally {
|
|
48062
|
+
rl2.close();
|
|
48063
|
+
}
|
|
47747
48064
|
} else {
|
|
47748
48065
|
console.log("Credentials saved to OS keychain.\n");
|
|
47749
48066
|
console.log(
|
|
@@ -48012,6 +48329,41 @@ var init_status = __esm({
|
|
|
48012
48329
|
}
|
|
48013
48330
|
});
|
|
48014
48331
|
|
|
48332
|
+
// src/cli/permissions.ts
|
|
48333
|
+
var permissions_exports = {};
|
|
48334
|
+
__export(permissions_exports, {
|
|
48335
|
+
runPermissions: () => runPermissions
|
|
48336
|
+
});
|
|
48337
|
+
async function runPermissions(profileArg) {
|
|
48338
|
+
const profile = profileArg || process.env.CONFLUENCE_PROFILE;
|
|
48339
|
+
if (!profile) {
|
|
48340
|
+
console.error(
|
|
48341
|
+
"Usage: epimethian-mcp permissions <profile>\nOr set CONFLUENCE_PROFILE in the environment."
|
|
48342
|
+
);
|
|
48343
|
+
process.exit(1);
|
|
48344
|
+
}
|
|
48345
|
+
if (!PROFILE_NAME_RE.test(profile)) {
|
|
48346
|
+
console.error(
|
|
48347
|
+
`Invalid profile name: "${profile}". Use lowercase alphanumeric and hyphens only (1-63 chars).`
|
|
48348
|
+
);
|
|
48349
|
+
process.exit(1);
|
|
48350
|
+
}
|
|
48351
|
+
process.env.CONFLUENCE_PROFILE = profile;
|
|
48352
|
+
const { getConfig: getConfig2, validateStartup: validateStartup2 } = await Promise.resolve().then(() => (init_confluence_client(), confluence_client_exports));
|
|
48353
|
+
const { buildCheckPermissionsPayload: buildCheckPermissionsPayload2 } = await Promise.resolve().then(() => (init_check_permissions(), check_permissions_exports));
|
|
48354
|
+
const config3 = await getConfig2();
|
|
48355
|
+
await validateStartup2(config3);
|
|
48356
|
+
const resolved = await getConfig2();
|
|
48357
|
+
const payload = buildCheckPermissionsPayload2(resolved);
|
|
48358
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
48359
|
+
}
|
|
48360
|
+
var init_permissions = __esm({
|
|
48361
|
+
"src/cli/permissions.ts"() {
|
|
48362
|
+
"use strict";
|
|
48363
|
+
init_keychain();
|
|
48364
|
+
}
|
|
48365
|
+
});
|
|
48366
|
+
|
|
48015
48367
|
// src/cli/fix-legacy-links.ts
|
|
48016
48368
|
var fix_legacy_links_exports = {};
|
|
48017
48369
|
__export(fix_legacy_links_exports, {
|
|
@@ -48324,7 +48676,7 @@ Informational:
|
|
|
48324
48676
|
var install_agent_default;
|
|
48325
48677
|
var init_install_agent = __esm({
|
|
48326
48678
|
"install-agent.md"() {
|
|
48327
|
-
install_agent_default = '# Epimethian MCP - Agent Installation Guide\n\n> This document is for AI agents installing and configuring the Epimethian MCP server on behalf of a user.\n\n## Verification\n\nThe official package is **`@de-otio/epimethian-mcp`** (scoped under `@de-otio`).\n\nDo NOT install unscoped alternatives like `epimethian-mcp` \u2014 these are unofficial and potentially unsafe.\n\n## Prerequisites\n\n- Node.js 18 or later\n- npm on PATH\n\n## Step 1: Install\n\n```bash\nnpm install -g @de-otio/epimethian-mcp\n```\n\nVerify the installation:\n\n```bash\nepimethian-mcp --version 2>/dev/null || which epimethian-mcp\n```\n\n## Step 2: Resolve the absolute path\n\nIDE-hosted agents may not resolve PATH correctly. Always use the absolute path in MCP configuration:\n\n```bash\nwhich epimethian-mcp\n```\n\nUse the output as the `command` value in the MCP config below.\n\n## Step 3: Collect configuration\n\nAsk the user for:\n1. **Profile name** \u2014 a short identifier for this Confluence instance (e.g., `globex`, `acme-corp`). Lowercase alphanumeric and hyphens only.\n2. **Confluence Cloud URL** \u2014 e.g., `https://yoursite.atlassian.net`\n3. **Email address** \u2014 the email associated with their Atlassian account\n\n## Step 4: Write MCP configuration\n\nAdd the server to `.mcp.json` (or the equivalent config file for the user\'s MCP client):\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path from Step 2>",\n "env": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>"\n }\n }\n }\n}\n```\n\n**IMPORTANT:** The only env var needed is `CONFLUENCE_PROFILE`. The URL, email, and API token are stored securely in the OS keychain \u2014 they should NOT appear in config files.\n\n## Step 5: Credential setup\n\nTell the user to run this command in their terminal:\n\n```\nepimethian-mcp setup --profile <profile name from Step 3>\n```\n\nThis interactive command will:\n1. Prompt for the Confluence URL, email, and API token (masked input)\n2. Test the connection\n3. Store all credentials securely in the OS keychain under the named profile\n\nThe API token is generated at: https://id.atlassian.com/manage-profile/security/api-tokens\n\n**Do NOT ask the user for the API token yourself.** The token must go directly from the user into the interactive setup command to avoid appearing in conversation logs.\n\n## Step 6: User must restart the MCP client\n\n**IMPORTANT:** The user must restart their MCP client (e.g., restart Claude Code, reload VS Code, restart Claude Desktop) for the new server configuration to take effect. The MCP client reads `.mcp.json` at startup and does not detect changes while running.\n\nTell the user:\n> Please restart your MCP client now to activate the Confluence tools.\n\n## Step 7: Validation\n\nAfter the user restarts, verify the server is working by listing available Confluence tools or running a simple operation like listing spaces.\n\n## Adding Additional Tenants\n\nTo add a second Confluence instance (e.g., for a different customer):\n\n1. Run `epimethian-mcp setup --profile <new-profile-name>` with the new credentials\n2. In the project that uses the new tenant, update `.mcp.json` to set `CONFLUENCE_PROFILE` to the new profile name\n3. Restart the MCP client\n\nEach VS Code window / Claude Code session uses the profile specified in its `.mcp.json`. Profiles are fully isolated \u2014 different OS keychain entries, different Confluence instances.\n\n## Managing Profiles\n\n- List all profiles: `epimethian-mcp profiles`\n- Show details: `epimethian-mcp profiles --verbose`\n- Check connection: `CONFLUENCE_PROFILE=<name> epimethian-mcp status`\n- Set read-only: `epimethian-mcp profiles --set-read-only <name>`\n- Set read-write: `epimethian-mcp profiles --set-read-write <name>`\n\n### Read-Only Mode\n\nNew profiles default to **read-only**. When read-only, all write tools are blocked and return an error. To enable writes for a profile:\n\n```bash\nepimethian-mcp profiles --set-read-write <name>\n```\n\nOr during setup: `epimethian-mcp setup --profile <name> --read-write`\n\n**Important:** Restart any running MCP servers after changing the read-only flag.\n\n### Removing a Profile\n\nTo delete a profile and its credentials, run:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\n**Agents must pass `--force`** because the command normally prompts for interactive confirmation (`Remove profile "<name>" and delete its credentials? [y/N]`), which will fail in non-TTY environments like agent shell sessions. The `--force` flag skips the confirmation prompt when stdin is not a TTY.\n\nThis command:\n1. Deletes the credential entry (URL, email, API token) from the OS keychain\n2. Removes the profile from the registry at `~/.config/epimethian-mcp/profiles.json`\n3. Writes an entry to the audit log at `~/.config/epimethian-mcp/audit.log`\n\nAfter removing a profile, also remove or update any `.mcp.json` files that reference it \u2014 otherwise the MCP server will fail to start with a missing-profile error.\n\n**Errors:**\n- If the profile name is invalid (not matching lowercase alphanumeric/hyphens, 1\u201363 chars), the command exits with code 1\n- If the profile does not exist in the keychain, the keychain deletion is silently skipped \u2014 the registry entry is still removed\n\n## Accessing This Guide Post-Install\n\nOnce installed, this guide is available locally via:\n\n```bash\nepimethian-mcp agent-guide\n```\n\nThis prints the full agent guide to stdout \u2014 no web fetch required.\n\n## Uninstallation\n\nWhen a user asks to uninstall Epimethian MCP, follow these steps:\n\n### Step 1: Check for existing profiles\n\n```bash\nepimethian-mcp profiles\n```\n\n### Step 2: Ask the user about credential cleanup\n\nIf profiles exist, ask the user:\n\n> You have Epimethian profiles configured: [list the profile names]. Would you like to delete all stored credentials before uninstalling? (This removes API tokens from your OS keychain.)\n\n### Step 3: Delete credentials (if the user agrees)\n\nFor each profile the user wants removed:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\nOr to remove all profiles:\n\n```bash\nfor name in $(epimethian-mcp profiles | grep \'^ \'); do epimethian-mcp profiles --remove "$name" --force; done\n```\n\n### Step 4: Remove MCP configuration\n\nDelete the `confluence` entry (or the tenant-specific entry like `confluence-globex`) from the project\'s `.mcp.json`.\n\n### Step 5: Uninstall the package\n\n```bash\nnpm uninstall -g @de-otio/epimethian-mcp\n```\n\n### Step 6: Restart the MCP client\n\nTell the user to restart their MCP client so it stops trying to launch the removed server.\n\n## CI/CD (No Keychain)\n\nFor environments where the OS keychain is unavailable (Docker, CI), set all three env vars directly:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path>",\n "env": {\n "CONFLUENCE_URL": "<url>",\n "CONFLUENCE_EMAIL": "<email>",\n "CONFLUENCE_API_TOKEN": "<token>"\n }\n }\n }\n}\n```\n\n**Warning:** This exposes the API token in the process environment. Use profile-based auth whenever possible.\n\n## Troubleshooting\n\nIf **npm install fails**:\n- Verify Node.js 18+ is installed: `node --version`\n- Verify npm is on PATH: `npm --version`\n- If permission errors occur, the user may need to fix their npm prefix or use a Node version manager (nvm, fnm)\n\nIf **`epimethian-mcp setup` fails**:\n- "Connection failed": Verify the Confluence URL is correct and accessible\n- "Token is invalid or expired": The user needs to generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens\n- Keychain errors on Linux: The user may need to install `libsecret` / `gnome-keyring` (`apt install libsecret-tools` or equivalent)\n\nIf **the server doesn\'t appear after restart**:\n- Verify the `.mcp.json` path is correct for the user\'s MCP client\n- Verify the `command` value is an absolute path (run `which epimethian-mcp` to confirm)\n- Check that `.mcp.json` contains valid JSON (no trailing commas, correct quoting)\n\n## Available Tools (33)\n\n| Tool | Description |\n|------|-------------|\n| `create_page` | Create a new Confluence page |\n| `get_page` | Read a page by ID (use `headings_only` to preview structure first) |\n| `get_page_by_title` | Look up a page by title (use `headings_only` to preview structure first) |\n| `update_page` | Update an existing page |\n| `update_page_section` | Update a single section by heading name |\n| `prepend_to_page` | Insert content at the beginning of an existing page (additive, safe) |\n| `append_to_page` | Insert content at the end of an existing page (additive, safe) |\n| `delete_page` | Delete a page |\n| `revert_page` | Revert a page to a previous version |\n| `list_pages` | List pages in a space |\n| `get_page_children` | Get child pages of a page |\n| `search_pages` | Search pages using CQL (Confluence Query Language) |\n| `get_spaces` | List available Confluence spaces |\n| `add_attachment` | Upload a file attachment to a page |\n| `get_attachments` | List attachments on a page |\n| `add_drawio_diagram` | Add a draw.io diagram to a page |\n| `get_labels` | Get all labels on a Confluence page |\n| `add_label` | Add one or more labels to a Confluence page |\n| `remove_label` | Remove a label from a Confluence page |\n| `get_page_status` | Get the content status badge on a page |\n| `set_page_status` | Set the content status badge on a page |\n| `remove_page_status` | Remove the content status badge from a page |\n| `get_comments` | Get footer and/or inline comments on a page |\n| `create_comment` | Create a footer or inline comment on a page |\n| `resolve_comment` | Resolve or reopen an inline comment |\n| `delete_comment` | Permanently delete a comment |\n| `get_page_versions` | List version history for a page |\n| `get_page_version` | Get page content at a specific historical version |\n| `diff_page_versions` | Compare two versions of a page |\n| `lookup_user` | Search for Atlassian users by name or email to resolve accountId for inline mentions |\n| `resolve_page_link` | Resolve a page title + space key to a stable contentId and URL for page links |\n| `get_version` | Return the epimethian-mcp server version and report available updates |\n| `upgrade` | Upgrade epimethian-mcp to the latest available version (restart required after) |\n';
|
|
48679
|
+
install_agent_default = '# Epimethian MCP - Agent Installation Guide\n\n> This document is for AI agents installing and configuring the Epimethian MCP server on behalf of a user.\n\n## Verification\n\nThe official package is **`@de-otio/epimethian-mcp`** (scoped under `@de-otio`).\n\nDo NOT install unscoped alternatives like `epimethian-mcp` \u2014 these are unofficial and potentially unsafe.\n\n## Prerequisites\n\n- Node.js 18 or later\n- npm on PATH\n\n## Step 1: Install\n\n```bash\nnpm install -g @de-otio/epimethian-mcp\n```\n\nVerify the installation:\n\n```bash\nepimethian-mcp --version 2>/dev/null || which epimethian-mcp\n```\n\n## Step 2: Resolve the absolute path\n\nIDE-hosted agents may not resolve PATH correctly. Always use the absolute path in MCP configuration:\n\n```bash\nwhich epimethian-mcp\n```\n\nUse the output as the `command` value in the MCP config below.\n\n## Step 3: Collect configuration\n\nAsk the user for:\n1. **Profile name** \u2014 a short identifier for this Confluence instance (e.g., `globex`, `acme-corp`). Lowercase alphanumeric and hyphens only.\n2. **Confluence Cloud URL** \u2014 e.g., `https://yoursite.atlassian.net`\n3. **Email address** \u2014 the email associated with their Atlassian account\n\n## Step 4: Write MCP configuration\n\nAdd the server to `.mcp.json` (or the equivalent config file for the user\'s MCP client):\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path from Step 2>",\n "env": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>"\n }\n }\n }\n}\n```\n\n**IMPORTANT:** The only env var needed is `CONFLUENCE_PROFILE`. The URL, email, and API token are stored securely in the OS keychain \u2014 they should NOT appear in config files.\n\n## Step 5: Credential setup\n\nTell the user to run this command in their terminal:\n\n```\nepimethian-mcp setup --profile <profile name from Step 3>\n```\n\nThis interactive command will:\n1. Prompt for the Confluence URL, email, and API token (masked input)\n2. Test the connection\n3. Store all credentials securely in the OS keychain under the named profile\n\nThe API token is generated at: https://id.atlassian.com/manage-profile/security/api-tokens\n\n**Do NOT ask the user for the API token yourself.** The token must go directly from the user into the interactive setup command to avoid appearing in conversation logs.\n\n## Step 6: User must restart the MCP client\n\n**IMPORTANT:** The user must restart their MCP client (e.g., restart Claude Code, reload VS Code, restart Claude Desktop) for the new server configuration to take effect. The MCP client reads `.mcp.json` at startup and does not detect changes while running.\n\nTell the user:\n> Please restart your MCP client now to activate the Confluence tools.\n\n## Step 7: Validation\n\nAfter the user restarts, verify the server is working by listing available Confluence tools or running a simple operation like listing spaces.\n\n## Adding Additional Tenants\n\nTo add a second Confluence instance (e.g., for a different customer):\n\n1. Run `epimethian-mcp setup --profile <new-profile-name>` with the new credentials\n2. In the project that uses the new tenant, update `.mcp.json` to set `CONFLUENCE_PROFILE` to the new profile name\n3. Restart the MCP client\n\nEach VS Code window / Claude Code session uses the profile specified in its `.mcp.json`. Profiles are fully isolated \u2014 different OS keychain entries, different Confluence instances.\n\n## Managing Profiles\n\n- List all profiles: `epimethian-mcp profiles`\n- Show details: `epimethian-mcp profiles --verbose`\n- Check connection: `CONFLUENCE_PROFILE=<name> epimethian-mcp status`\n- Set read-only: `epimethian-mcp profiles --set-read-only <name>`\n- Set read-write: `epimethian-mcp profiles --set-read-write <name>`\n\n### Read-Only Mode\n\nNew profiles default to **read-only**. When read-only, all write tools are blocked and return an error. To enable writes for a profile:\n\n```bash\nepimethian-mcp profiles --set-read-write <name>\n```\n\nOr during setup: `epimethian-mcp setup --profile <name> --read-write`\n\n**Important:** Restart any running MCP servers after changing the read-only flag.\n\n### Removing a Profile\n\nTo delete a profile and its credentials, run:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\n**Agents must pass `--force`** because the command normally prompts for interactive confirmation (`Remove profile "<name>" and delete its credentials? [y/N]`), which will fail in non-TTY environments like agent shell sessions. The `--force` flag skips the confirmation prompt when stdin is not a TTY.\n\nThis command:\n1. Deletes the credential entry (URL, email, API token) from the OS keychain\n2. Removes the profile from the registry at `~/.config/epimethian-mcp/profiles.json`\n3. Writes an entry to the audit log at `~/.config/epimethian-mcp/audit.log`\n\nAfter removing a profile, also remove or update any `.mcp.json` files that reference it \u2014 otherwise the MCP server will fail to start with a missing-profile error.\n\n**Errors:**\n- If the profile name is invalid (not matching lowercase alphanumeric/hyphens, 1\u201363 chars), the command exits with code 1\n- If the profile does not exist in the keychain, the keychain deletion is silently skipped \u2014 the registry entry is still removed\n\n## Accessing This Guide Post-Install\n\nOnce installed, this guide is available locally via:\n\n```bash\nepimethian-mcp agent-guide\n```\n\nThis prints the full agent guide to stdout \u2014 no web fetch required.\n\n## Uninstallation\n\nWhen a user asks to uninstall Epimethian MCP, follow these steps:\n\n### Step 1: Check for existing profiles\n\n```bash\nepimethian-mcp profiles\n```\n\n### Step 2: Ask the user about credential cleanup\n\nIf profiles exist, ask the user:\n\n> You have Epimethian profiles configured: [list the profile names]. Would you like to delete all stored credentials before uninstalling? (This removes API tokens from your OS keychain.)\n\n### Step 3: Delete credentials (if the user agrees)\n\nFor each profile the user wants removed:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\nOr to remove all profiles:\n\n```bash\nfor name in $(epimethian-mcp profiles | grep \'^ \'); do epimethian-mcp profiles --remove "$name" --force; done\n```\n\n### Step 4: Remove MCP configuration\n\nDelete the `confluence` entry (or the tenant-specific entry like `confluence-globex`) from the project\'s `.mcp.json`.\n\n### Step 5: Uninstall the package\n\n```bash\nnpm uninstall -g @de-otio/epimethian-mcp\n```\n\n### Step 6: Restart the MCP client\n\nTell the user to restart their MCP client so it stops trying to launch the removed server.\n\n## CI/CD (No Keychain)\n\nFor environments where the OS keychain is unavailable (Docker, CI), set all three env vars directly:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path>",\n "env": {\n "CONFLUENCE_URL": "<url>",\n "CONFLUENCE_EMAIL": "<email>",\n "CONFLUENCE_API_TOKEN": "<token>"\n }\n }\n }\n}\n```\n\n**Warning:** This exposes the API token in the process environment. Use profile-based auth whenever possible.\n\n## Troubleshooting\n\nIf **npm install fails**:\n- Verify Node.js 18+ is installed: `node --version`\n- Verify npm is on PATH: `npm --version`\n- If permission errors occur, the user may need to fix their npm prefix or use a Node version manager (nvm, fnm)\n\nIf **`epimethian-mcp setup` fails**:\n- "Connection failed": Verify the Confluence URL is correct and accessible\n- "Token is invalid or expired": The user needs to generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens\n- Keychain errors on Linux: The user may need to install `libsecret` / `gnome-keyring` (`apt install libsecret-tools` or equivalent)\n\nIf **the server doesn\'t appear after restart**:\n- Verify the `.mcp.json` path is correct for the user\'s MCP client\n- Verify the `command` value is an absolute path (run `which epimethian-mcp` to confirm)\n- Check that `.mcp.json` contains valid JSON (no trailing commas, correct quoting)\n\n## Available Tools (34)\n\n| Tool | Description |\n|------|-------------|\n| `check_permissions` | Report the current profile\'s MCP access mode and the token\'s capabilities |\n| `create_page` | Create a new Confluence page |\n| `get_page` | Read a page by ID (use `headings_only` to preview structure first) |\n| `get_page_by_title` | Look up a page by title (use `headings_only` to preview structure first) |\n| `update_page` | Update an existing page |\n| `update_page_section` | Update a single section by heading name |\n| `prepend_to_page` | Insert content at the beginning of an existing page (additive, safe) |\n| `append_to_page` | Insert content at the end of an existing page (additive, safe) |\n| `delete_page` | Delete a page |\n| `revert_page` | Revert a page to a previous version |\n| `list_pages` | List pages in a space |\n| `get_page_children` | Get child pages of a page |\n| `search_pages` | Search pages using CQL (Confluence Query Language) |\n| `get_spaces` | List available Confluence spaces |\n| `add_attachment` | Upload a file attachment to a page |\n| `get_attachments` | List attachments on a page |\n| `add_drawio_diagram` | Add a draw.io diagram to a page |\n| `get_labels` | Get all labels on a Confluence page |\n| `add_label` | Add one or more labels to a Confluence page |\n| `remove_label` | Remove a label from a Confluence page |\n| `get_page_status` | Get the content status badge on a page |\n| `set_page_status` | Set the content status badge on a page |\n| `remove_page_status` | Remove the content status badge from a page |\n| `get_comments` | Get footer and/or inline comments on a page |\n| `create_comment` | Create a footer or inline comment on a page |\n| `resolve_comment` | Resolve or reopen an inline comment |\n| `delete_comment` | Permanently delete a comment |\n| `get_page_versions` | List version history for a page |\n| `get_page_version` | Get page content at a specific historical version |\n| `diff_page_versions` | Compare two versions of a page |\n| `lookup_user` | Search for Atlassian users by name or email to resolve accountId for inline mentions |\n| `resolve_page_link` | Resolve a page title + space key to a stable contentId and URL for page links |\n| `get_version` | Return the epimethian-mcp server version and report available updates |\n| `upgrade` | Upgrade epimethian-mcp to the latest available version (restart required after) |\n';
|
|
48328
48680
|
}
|
|
48329
48681
|
});
|
|
48330
48682
|
|
|
@@ -48349,7 +48701,7 @@ __export(upgrade_exports, {
|
|
|
48349
48701
|
runUpgrade: () => runUpgrade
|
|
48350
48702
|
});
|
|
48351
48703
|
async function runUpgrade() {
|
|
48352
|
-
const currentVersion = "6.0
|
|
48704
|
+
const currentVersion = "6.1.0";
|
|
48353
48705
|
console.log(`epimethian-mcp upgrade: current version v${currentVersion}`);
|
|
48354
48706
|
let pending = await getPendingUpdate();
|
|
48355
48707
|
if (!pending) {
|
|
@@ -59162,6 +59514,81 @@ function storageToMarkdown(storage) {
|
|
|
59162
59514
|
|
|
59163
59515
|
// src/server/index.ts
|
|
59164
59516
|
init_mutation_log();
|
|
59517
|
+
|
|
59518
|
+
// src/server/provenance.ts
|
|
59519
|
+
init_confluence_client();
|
|
59520
|
+
var UNVERIFIED_COLOR = "#FFC400";
|
|
59521
|
+
var UNVERIFIED_LABELS = {
|
|
59522
|
+
en: "AI-edited",
|
|
59523
|
+
fr: "Modifi\xE9 par IA",
|
|
59524
|
+
de: "KI-bearbeitet",
|
|
59525
|
+
es: "Editado por IA",
|
|
59526
|
+
pt: "Editado por IA",
|
|
59527
|
+
it: "Modificato da IA",
|
|
59528
|
+
nl: "AI-bewerkt",
|
|
59529
|
+
ja: "AI\u7DE8\u96C6\u6E08\u307F",
|
|
59530
|
+
zh: "AI\u5DF2\u7F16\u8F91",
|
|
59531
|
+
ko: "AI \uD3B8\uC9D1\uB428"
|
|
59532
|
+
};
|
|
59533
|
+
for (const [locale, label] of Object.entries(UNVERIFIED_LABELS)) {
|
|
59534
|
+
const codePoints = [...label].length;
|
|
59535
|
+
if (codePoints > 20) {
|
|
59536
|
+
throw new Error(
|
|
59537
|
+
`UNVERIFIED_LABELS["${locale}"] = "${label}" has ${codePoints} code points, exceeding the 20-code-point Confluence limit.`
|
|
59538
|
+
);
|
|
59539
|
+
}
|
|
59540
|
+
}
|
|
59541
|
+
var KNOWN_LABELS = new Set(Object.values(UNVERIFIED_LABELS));
|
|
59542
|
+
function isKnownUnverifiedLabel(name, customOverride) {
|
|
59543
|
+
if (customOverride !== void 0 && name === customOverride) return true;
|
|
59544
|
+
return KNOWN_LABELS.has(name);
|
|
59545
|
+
}
|
|
59546
|
+
function pickLocale(cfg) {
|
|
59547
|
+
const raw = cfg.unverifiedStatusLocale || process.env.CONFLUENCE_UNVERIFIED_STATUS_LOCALE || Intl.DateTimeFormat().resolvedOptions().locale || "en";
|
|
59548
|
+
return raw.split("-")[0].toLowerCase();
|
|
59549
|
+
}
|
|
59550
|
+
function resolveUnverifiedStatus(cfg) {
|
|
59551
|
+
const color = cfg.unverifiedStatusColor ?? UNVERIFIED_COLOR;
|
|
59552
|
+
if (cfg.unverifiedStatusName) {
|
|
59553
|
+
return { name: cfg.unverifiedStatusName, color };
|
|
59554
|
+
}
|
|
59555
|
+
const locale = pickLocale(cfg);
|
|
59556
|
+
const name = UNVERIFIED_LABELS[locale] ?? UNVERIFIED_LABELS["en"];
|
|
59557
|
+
return { name, color };
|
|
59558
|
+
}
|
|
59559
|
+
async function markPageUnverified(pageId, cfg) {
|
|
59560
|
+
if (cfg.unverifiedStatus === false) {
|
|
59561
|
+
return {};
|
|
59562
|
+
}
|
|
59563
|
+
const target = resolveUnverifiedStatus(cfg);
|
|
59564
|
+
let skipSet = false;
|
|
59565
|
+
try {
|
|
59566
|
+
const current = await getContentState(pageId);
|
|
59567
|
+
if (current != null && current.color === target.color && isKnownUnverifiedLabel(current.name, cfg.unverifiedStatusName)) {
|
|
59568
|
+
return {};
|
|
59569
|
+
}
|
|
59570
|
+
} catch (err) {
|
|
59571
|
+
if (err instanceof ConfluencePermissionError) {
|
|
59572
|
+
}
|
|
59573
|
+
}
|
|
59574
|
+
if (skipSet) return {};
|
|
59575
|
+
try {
|
|
59576
|
+
await setContentState(pageId, target.name, target.color);
|
|
59577
|
+
return {};
|
|
59578
|
+
} catch (err) {
|
|
59579
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
59580
|
+
if (err instanceof ConfluencePermissionError) {
|
|
59581
|
+
return {
|
|
59582
|
+
warning: `Could not apply 'AI-edited' status badge (permission denied). Provenance badge is missing for page ${pageId}.`
|
|
59583
|
+
};
|
|
59584
|
+
}
|
|
59585
|
+
return {
|
|
59586
|
+
warning: `Could not apply 'AI-edited' status badge: ${message}. Provenance badge is missing for page ${pageId}.`
|
|
59587
|
+
};
|
|
59588
|
+
}
|
|
59589
|
+
}
|
|
59590
|
+
|
|
59591
|
+
// src/server/index.ts
|
|
59165
59592
|
init_safe_write();
|
|
59166
59593
|
|
|
59167
59594
|
// src/server/source-provenance.ts
|
|
@@ -59427,6 +59854,7 @@ async function assertSpaceAllowed(opts) {
|
|
|
59427
59854
|
}
|
|
59428
59855
|
|
|
59429
59856
|
// src/server/index.ts
|
|
59857
|
+
init_check_permissions();
|
|
59430
59858
|
init_update_check();
|
|
59431
59859
|
function getClientLabel(server) {
|
|
59432
59860
|
const client = server.server.getClientVersion();
|
|
@@ -59481,7 +59909,16 @@ ${markdown}`;
|
|
|
59481
59909
|
|
|
59482
59910
|
${body}`;
|
|
59483
59911
|
}
|
|
59912
|
+
var _sessionIsReadOnly = false;
|
|
59913
|
+
var _readOnlyNoteEmitted = false;
|
|
59484
59914
|
function toolResult(text2) {
|
|
59915
|
+
if (_sessionIsReadOnly && !_readOnlyNoteEmitted) {
|
|
59916
|
+
_readOnlyNoteEmitted = true;
|
|
59917
|
+
const note = "[epimethian-mcp] This profile is read-only; write tools are not exposed.";
|
|
59918
|
+
return { content: [{ type: "text", text: `${note}
|
|
59919
|
+
|
|
59920
|
+
${text2}` }] };
|
|
59921
|
+
}
|
|
59485
59922
|
return { content: [{ type: "text", text: text2 }] };
|
|
59486
59923
|
}
|
|
59487
59924
|
function toolError(err) {
|
|
@@ -59489,6 +59926,42 @@ function toolError(err) {
|
|
|
59489
59926
|
const message = sanitizeError(raw);
|
|
59490
59927
|
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
59491
59928
|
}
|
|
59929
|
+
function toolErrorWithContext(err, ctx) {
|
|
59930
|
+
if (err instanceof ConfluenceAuthError) {
|
|
59931
|
+
const profileName = ctx.profile ?? "<profile>";
|
|
59932
|
+
return {
|
|
59933
|
+
content: [{
|
|
59934
|
+
type: "text",
|
|
59935
|
+
text: `Error: Your Confluence API token is invalid or expired. Reauthenticate with 'epimethian-mcp login ${profileName}'.`
|
|
59936
|
+
}],
|
|
59937
|
+
isError: true
|
|
59938
|
+
};
|
|
59939
|
+
}
|
|
59940
|
+
if (err instanceof ConfluencePermissionError) {
|
|
59941
|
+
const resourcePart = ctx.resource ? ` on ${ctx.resource}` : "";
|
|
59942
|
+
return {
|
|
59943
|
+
content: [{
|
|
59944
|
+
type: "text",
|
|
59945
|
+
text: `Error: Your token lacks permission for ${ctx.operation}${resourcePart}. The operation was not performed.`
|
|
59946
|
+
}],
|
|
59947
|
+
isError: true
|
|
59948
|
+
};
|
|
59949
|
+
}
|
|
59950
|
+
if (err instanceof ConfluenceNotFoundError) {
|
|
59951
|
+
return {
|
|
59952
|
+
content: [{
|
|
59953
|
+
type: "text",
|
|
59954
|
+
text: `Error: Resource not found. Confluence may return 'not found' when a token lacks permission to see the resource \u2014 verify the token has at least read access.`
|
|
59955
|
+
}],
|
|
59956
|
+
isError: true
|
|
59957
|
+
};
|
|
59958
|
+
}
|
|
59959
|
+
return toolError(err);
|
|
59960
|
+
}
|
|
59961
|
+
function appendWarnings(primary, warnings) {
|
|
59962
|
+
if (warnings.length === 0) return primary;
|
|
59963
|
+
return primary + "\n\n" + warnings.map((w) => `\u26A0 ${w}`).join("\n");
|
|
59964
|
+
}
|
|
59492
59965
|
function tenantEcho(config3) {
|
|
59493
59966
|
const host = new URL(config3.url).hostname;
|
|
59494
59967
|
const mode = config3.profile ? `profile: ${config3.profile}` : "env-var mode";
|
|
@@ -59517,6 +59990,24 @@ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
|
59517
59990
|
"lookup_user",
|
|
59518
59991
|
"resolve_page_link"
|
|
59519
59992
|
]);
|
|
59993
|
+
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
59994
|
+
"create_page",
|
|
59995
|
+
"update_page",
|
|
59996
|
+
"append_to_page",
|
|
59997
|
+
"prepend_to_page",
|
|
59998
|
+
"update_page_section",
|
|
59999
|
+
"delete_page",
|
|
60000
|
+
"add_drawio_diagram",
|
|
60001
|
+
"revert_page",
|
|
60002
|
+
"add_attachment",
|
|
60003
|
+
"add_label",
|
|
60004
|
+
"remove_label",
|
|
60005
|
+
"create_comment",
|
|
60006
|
+
"delete_comment",
|
|
60007
|
+
"resolve_comment",
|
|
60008
|
+
"set_page_status",
|
|
60009
|
+
"remove_page_status"
|
|
60010
|
+
]);
|
|
59520
60011
|
function writeGuard(toolName, config3) {
|
|
59521
60012
|
if (!config3.readOnly) return null;
|
|
59522
60013
|
if (READ_ONLY_TOOLS.has(toolName)) return null;
|
|
@@ -59575,27 +60066,38 @@ function formatComments(footer, inline2, pageId) {
|
|
|
59575
60066
|
}
|
|
59576
60067
|
return lines.join("\n");
|
|
59577
60068
|
}
|
|
59578
|
-
function formatCommentThreads(footer, inline2, pageId) {
|
|
60069
|
+
function formatCommentThreads(footer, inline2, pageId, failedFetches = 0, totalFetches = 0) {
|
|
59579
60070
|
const lines = [`Comments on page ${pageId}:`, ""];
|
|
59580
60071
|
if (footer.length > 0) {
|
|
59581
60072
|
lines.push(`Footer comments (${footer.length}):`);
|
|
59582
|
-
footer.forEach(({ comment: comment2, replies }) => {
|
|
60073
|
+
footer.forEach(({ comment: comment2, replies, error: error2 }) => {
|
|
59583
60074
|
lines.push(formatCommentLine(comment2));
|
|
59584
|
-
|
|
60075
|
+
if (error2) {
|
|
60076
|
+
lines.push(` Error fetching replies: ${error2}`);
|
|
60077
|
+
} else if (replies) {
|
|
60078
|
+
replies.forEach((r) => lines.push(formatCommentLine(r, " ")));
|
|
60079
|
+
}
|
|
59585
60080
|
});
|
|
59586
60081
|
lines.push("");
|
|
59587
60082
|
}
|
|
59588
60083
|
if (inline2.length > 0) {
|
|
59589
60084
|
lines.push(`Inline comments (${inline2.length}):`);
|
|
59590
|
-
inline2.forEach(({ comment: comment2, replies }) => {
|
|
60085
|
+
inline2.forEach(({ comment: comment2, replies, error: error2 }) => {
|
|
59591
60086
|
lines.push(formatCommentLine(comment2));
|
|
59592
|
-
|
|
60087
|
+
if (error2) {
|
|
60088
|
+
lines.push(` Error fetching replies: ${error2}`);
|
|
60089
|
+
} else if (replies) {
|
|
60090
|
+
replies.forEach((r) => lines.push(formatCommentLine(r, " ")));
|
|
60091
|
+
}
|
|
59593
60092
|
});
|
|
59594
60093
|
lines.push("");
|
|
59595
60094
|
}
|
|
59596
60095
|
if (footer.length === 0 && inline2.length === 0) {
|
|
59597
60096
|
lines.push("No comments found.");
|
|
59598
60097
|
}
|
|
60098
|
+
if (failedFetches > 0 && totalFetches > 0) {
|
|
60099
|
+
lines.push(`Note: ${failedFetches} of ${totalFetches} reply fetches failed \u2014 partial results shown.`);
|
|
60100
|
+
}
|
|
59599
60101
|
return lines.join("\n");
|
|
59600
60102
|
}
|
|
59601
60103
|
async function registerTools(server, config3) {
|
|
@@ -59604,11 +60106,30 @@ async function registerTools(server, config3) {
|
|
|
59604
60106
|
const isToolEnabled = resolveToolFilter(settings);
|
|
59605
60107
|
const allowedSpaces = settings?.spaces;
|
|
59606
60108
|
const checkSpaceAllowed = (opts) => assertSpaceAllowed({ spaces: allowedSpaces, ...opts });
|
|
60109
|
+
const effectivePosture = config3.effectivePosture ?? "read-write";
|
|
60110
|
+
const isReadOnly = effectivePosture === "read-only";
|
|
60111
|
+
const profileLabel = config3.profile ? `"${config3.profile}"` : `"env-var"`;
|
|
60112
|
+
const sourceLabel = config3.postureSource ?? "default";
|
|
60113
|
+
if (isReadOnly) {
|
|
60114
|
+
console.error(
|
|
60115
|
+
`[epimethian-mcp] Profile ${profileLabel} \u2014 mode: read-only (${sourceLabel}).
|
|
60116
|
+
Write tools are not exposed. Set posture: "read-write" in the profile to enable writes.`
|
|
60117
|
+
);
|
|
60118
|
+
} else {
|
|
60119
|
+
console.error(
|
|
60120
|
+
`[epimethian-mcp] Profile ${profileLabel} \u2014 mode: read-write (${sourceLabel}).`
|
|
60121
|
+
);
|
|
60122
|
+
}
|
|
60123
|
+
_sessionIsReadOnly = isReadOnly;
|
|
60124
|
+
_readOnlyNoteEmitted = false;
|
|
59607
60125
|
const originalRegisterTool = server.registerTool.bind(server);
|
|
59608
60126
|
server.registerTool = function(name, ...rest) {
|
|
59609
60127
|
if (!isToolEnabled(name)) {
|
|
59610
60128
|
return server;
|
|
59611
60129
|
}
|
|
60130
|
+
if (isReadOnly && WRITE_TOOLS.has(name)) {
|
|
60131
|
+
return server;
|
|
60132
|
+
}
|
|
59612
60133
|
return originalRegisterTool(name, ...rest);
|
|
59613
60134
|
};
|
|
59614
60135
|
const labelNameSchema = external_exports.string().min(1).max(255).regex(/^[a-z0-9][a-z0-9_-]*$/, "Label must be lowercase alphanumeric, hyphens, underscores only");
|
|
@@ -59698,9 +60219,14 @@ async function registerTools(server, config3) {
|
|
|
59698
60219
|
deletedTokens: prepared.deletedTokens,
|
|
59699
60220
|
clientLabel: getClientLabel(server)
|
|
59700
60221
|
});
|
|
59701
|
-
|
|
60222
|
+
const warnings = [];
|
|
60223
|
+
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
60224
|
+
if (labelResult.warning) warnings.push(labelResult.warning);
|
|
60225
|
+
const badgeResult = await markPageUnverified(submitted.page.id, cfg);
|
|
60226
|
+
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
60227
|
+
return toolResult(appendWarnings(await formatPage(submitted.page, false), warnings) + echo);
|
|
59702
60228
|
} catch (err) {
|
|
59703
|
-
return
|
|
60229
|
+
return toolErrorWithContext(err, { operation: "create_page", resource: `space ${space_key}`, profile: config3.profile });
|
|
59704
60230
|
}
|
|
59705
60231
|
}
|
|
59706
60232
|
);
|
|
@@ -59895,17 +60421,22 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
59895
60421
|
source: effectiveSource
|
|
59896
60422
|
});
|
|
59897
60423
|
const isTitleOnly = prepared.finalStorage === void 0;
|
|
60424
|
+
const warnings = [];
|
|
60425
|
+
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
60426
|
+
if (labelResult.warning) warnings.push(labelResult.warning);
|
|
60427
|
+
const badgeResult = await markPageUnverified(submitted.page.id, cfg);
|
|
60428
|
+
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
59898
60429
|
if (isTitleOnly) {
|
|
59899
60430
|
return toolResult(
|
|
59900
|
-
`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, title only, body unchanged)
|
|
60431
|
+
appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, title only, body unchanged)`, warnings) + echo
|
|
59901
60432
|
);
|
|
59902
60433
|
}
|
|
59903
60434
|
const removalNote = submitted.deletedTokens.length > 0 ? `; removed ${submitted.deletedTokens.length} preserved macro${submitted.deletedTokens.length === 1 ? "" : "s"}: ${submitted.deletedTokens.map((t) => t.fingerprint).join(", ")}` : "";
|
|
59904
60435
|
return toolResult(
|
|
59905
|
-
`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars${removalNote})
|
|
60436
|
+
appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars${removalNote})`, warnings) + echo
|
|
59906
60437
|
);
|
|
59907
60438
|
} catch (err) {
|
|
59908
|
-
return
|
|
60439
|
+
return toolErrorWithContext(err, { operation: "update_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
59909
60440
|
}
|
|
59910
60441
|
}
|
|
59911
60442
|
);
|
|
@@ -59967,7 +60498,7 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
59967
60498
|
return toolResult(`Deleted page ${page_id}` + echo);
|
|
59968
60499
|
} catch (err) {
|
|
59969
60500
|
logMutation(errorRecord("delete_page", page_id, err));
|
|
59970
|
-
return
|
|
60501
|
+
return toolErrorWithContext(err, { operation: "delete_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
59971
60502
|
}
|
|
59972
60503
|
}
|
|
59973
60504
|
);
|
|
@@ -60033,12 +60564,17 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
60033
60564
|
operation: "update_page_section",
|
|
60034
60565
|
clientLabel: getClientLabel(server)
|
|
60035
60566
|
});
|
|
60567
|
+
const warnings = [];
|
|
60568
|
+
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
60569
|
+
if (labelResult.warning) warnings.push(labelResult.warning);
|
|
60570
|
+
const badgeResult = await markPageUnverified(submitted.page.id, cfg);
|
|
60571
|
+
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
60036
60572
|
const removalNote = submitted.deletedTokens.length > 0 ? `; removed ${submitted.deletedTokens.length} preserved macro${submitted.deletedTokens.length === 1 ? "" : "s"}: ${submitted.deletedTokens.map((t) => t.fingerprint).join(", ")}` : "";
|
|
60037
60573
|
return toolResult(
|
|
60038
|
-
`Updated section "${section}" in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})
|
|
60574
|
+
appendWarnings(`Updated section "${section}" in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})`, warnings) + echo
|
|
60039
60575
|
);
|
|
60040
60576
|
} catch (err) {
|
|
60041
|
-
return
|
|
60577
|
+
return toolErrorWithContext(err, { operation: "update_page_section", resource: `page ${page_id}`, profile: config3.profile });
|
|
60042
60578
|
}
|
|
60043
60579
|
}
|
|
60044
60580
|
);
|
|
@@ -60075,9 +60611,14 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
60075
60611
|
"prepend",
|
|
60076
60612
|
{ separator, versionMessage: version_message ?? "Prepend content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url }
|
|
60077
60613
|
);
|
|
60078
|
-
|
|
60614
|
+
const warnings = [];
|
|
60615
|
+
const labelResult = await ensureAttributionLabel(page.id);
|
|
60616
|
+
if (labelResult.warning) warnings.push(labelResult.warning);
|
|
60617
|
+
const badgeResult = await markPageUnverified(page.id, cfg);
|
|
60618
|
+
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
60619
|
+
return toolResult(appendWarnings(`Prepended to: ${page.title} (ID: ${page.id}, version: ${newVersion}, body: ${oldLen}\u2192${newLen} chars)`, warnings) + echo);
|
|
60079
60620
|
} catch (err) {
|
|
60080
|
-
return
|
|
60621
|
+
return toolErrorWithContext(err, { operation: "prepend_to_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
60081
60622
|
}
|
|
60082
60623
|
}
|
|
60083
60624
|
);
|
|
@@ -60114,9 +60655,14 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
60114
60655
|
"append",
|
|
60115
60656
|
{ separator, versionMessage: version_message ?? "Append content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url }
|
|
60116
60657
|
);
|
|
60117
|
-
|
|
60658
|
+
const warnings = [];
|
|
60659
|
+
const labelResult = await ensureAttributionLabel(page.id);
|
|
60660
|
+
if (labelResult.warning) warnings.push(labelResult.warning);
|
|
60661
|
+
const badgeResult = await markPageUnverified(page.id, cfg);
|
|
60662
|
+
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
60663
|
+
return toolResult(appendWarnings(`Appended to: ${page.title} (ID: ${page.id}, version: ${newVersion}, body: ${oldLen}\u2192${newLen} chars)`, warnings) + echo);
|
|
60118
60664
|
} catch (err) {
|
|
60119
|
-
return
|
|
60665
|
+
return toolErrorWithContext(err, { operation: "append_to_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
60120
60666
|
}
|
|
60121
60667
|
}
|
|
60122
60668
|
);
|
|
@@ -60239,6 +60785,23 @@ ${truncated}${truncationNote(origLen)}`
|
|
|
60239
60785
|
}
|
|
60240
60786
|
}
|
|
60241
60787
|
);
|
|
60788
|
+
server.registerTool(
|
|
60789
|
+
"check_permissions",
|
|
60790
|
+
{
|
|
60791
|
+
description: "Report the current profile's MCP access mode and the token's capabilities. Always available in every posture.",
|
|
60792
|
+
inputSchema: {},
|
|
60793
|
+
annotations: { readOnlyHint: true }
|
|
60794
|
+
},
|
|
60795
|
+
async () => {
|
|
60796
|
+
try {
|
|
60797
|
+
const cfg = await getConfig();
|
|
60798
|
+
const payload = buildCheckPermissionsPayload(cfg);
|
|
60799
|
+
return toolResult(JSON.stringify(payload, null, 2));
|
|
60800
|
+
} catch (err) {
|
|
60801
|
+
return toolError(err);
|
|
60802
|
+
}
|
|
60803
|
+
}
|
|
60804
|
+
);
|
|
60242
60805
|
server.registerTool(
|
|
60243
60806
|
"get_page_by_title",
|
|
60244
60807
|
{
|
|
@@ -60374,7 +60937,7 @@ ${truncated}`);
|
|
|
60374
60937
|
`Attached: ${att.title} (ID: ${att.id}, size: ${att.fileSize ?? "unknown"} bytes) to page ${page_id}` + echo
|
|
60375
60938
|
);
|
|
60376
60939
|
} catch (err) {
|
|
60377
|
-
return
|
|
60940
|
+
return toolErrorWithContext(err, { operation: "add_attachment", resource: `page ${page_id}`, profile: config3.profile });
|
|
60378
60941
|
}
|
|
60379
60942
|
}
|
|
60380
60943
|
);
|
|
@@ -60458,11 +61021,16 @@ ${macro}` : macro;
|
|
|
60458
61021
|
clientLabel: getClientLabel(server),
|
|
60459
61022
|
operation: "add_drawio_diagram"
|
|
60460
61023
|
});
|
|
61024
|
+
const warnings = [];
|
|
61025
|
+
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
61026
|
+
if (labelResult.warning) warnings.push(labelResult.warning);
|
|
61027
|
+
const badgeResult = await markPageUnverified(submitted.page.id, config3);
|
|
61028
|
+
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
60461
61029
|
return toolResult(
|
|
60462
|
-
`Diagram "${filename}" added to page ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion})
|
|
61030
|
+
appendWarnings(`Diagram "${filename}" added to page ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion})`, warnings) + echo
|
|
60463
61031
|
);
|
|
60464
61032
|
} catch (err) {
|
|
60465
|
-
return
|
|
61033
|
+
return toolErrorWithContext(err, { operation: "add_drawio_diagram", resource: `page ${page_id}`, profile: config3.profile });
|
|
60466
61034
|
}
|
|
60467
61035
|
}
|
|
60468
61036
|
);
|
|
@@ -60543,7 +61111,7 @@ ${lines}`);
|
|
|
60543
61111
|
await addLabels(page_id, labels);
|
|
60544
61112
|
return toolResult(`Added ${labels.length} label(s) to page ${page_id}: ${labels.join(", ")}` + echo);
|
|
60545
61113
|
} catch (err) {
|
|
60546
|
-
return
|
|
61114
|
+
return toolErrorWithContext(err, { operation: "add_label", resource: `page ${page_id}`, profile: config3.profile });
|
|
60547
61115
|
}
|
|
60548
61116
|
}
|
|
60549
61117
|
);
|
|
@@ -60568,7 +61136,7 @@ ${lines}`);
|
|
|
60568
61136
|
await removeLabel(page_id, label);
|
|
60569
61137
|
return toolResult(`Removed label "${label}" from page ${page_id}` + echo);
|
|
60570
61138
|
} catch (err) {
|
|
60571
|
-
return
|
|
61139
|
+
return toolErrorWithContext(err, { operation: "remove_label", resource: `page ${page_id}`, profile: config3.profile });
|
|
60572
61140
|
}
|
|
60573
61141
|
}
|
|
60574
61142
|
);
|
|
@@ -60641,7 +61209,7 @@ Color: ${state.color}` + echo
|
|
|
60641
61209
|
await setContentState(page_id, name, color);
|
|
60642
61210
|
return toolResult(`Set status on page ${page_id}: "${name}" (${color})` + echo);
|
|
60643
61211
|
} catch (err) {
|
|
60644
|
-
return
|
|
61212
|
+
return toolErrorWithContext(err, { operation: "set_page_status", resource: `page ${page_id}`, profile: config3.profile });
|
|
60645
61213
|
}
|
|
60646
61214
|
}
|
|
60647
61215
|
);
|
|
@@ -60667,7 +61235,7 @@ Color: ${state.color}` + echo
|
|
|
60667
61235
|
await removeContentState(page_id);
|
|
60668
61236
|
return toolResult(`Removed status from page ${page_id}` + echo);
|
|
60669
61237
|
} catch (err) {
|
|
60670
|
-
return
|
|
61238
|
+
return toolErrorWithContext(err, { operation: "remove_page_status", resource: `page ${page_id}`, profile: config3.profile });
|
|
60671
61239
|
}
|
|
60672
61240
|
}
|
|
60673
61241
|
);
|
|
@@ -60692,17 +61260,34 @@ Color: ${state.color}` + echo
|
|
|
60692
61260
|
type !== "footer" ? getInlineComments(page_id, resolution_status) : Promise.resolve([])
|
|
60693
61261
|
]);
|
|
60694
61262
|
if (include_replies) {
|
|
60695
|
-
const
|
|
60696
|
-
|
|
61263
|
+
const footerRepliesResults = await Promise.allSettled(
|
|
61264
|
+
footerComments.map((c) => getCommentReplies(c.id, "footer"))
|
|
61265
|
+
);
|
|
61266
|
+
const inlineRepliesResults = await Promise.allSettled(
|
|
61267
|
+
inlineComments.map((c) => getCommentReplies(c.id, "inline"))
|
|
61268
|
+
);
|
|
61269
|
+
const fr = footerComments.map((c, i) => {
|
|
61270
|
+
const result = footerRepliesResults[i];
|
|
61271
|
+
return {
|
|
60697
61272
|
comment: c,
|
|
60698
|
-
replies:
|
|
60699
|
-
}
|
|
60700
|
-
|
|
61273
|
+
...result.status === "fulfilled" ? { replies: result.value } : { error: result.reason instanceof Error ? result.reason.message : String(result.reason) }
|
|
61274
|
+
};
|
|
61275
|
+
});
|
|
61276
|
+
const ir = inlineComments.map((c, i) => {
|
|
61277
|
+
const result = inlineRepliesResults[i];
|
|
61278
|
+
return {
|
|
60701
61279
|
comment: c,
|
|
60702
|
-
replies:
|
|
60703
|
-
}
|
|
60704
|
-
|
|
60705
|
-
|
|
61280
|
+
...result.status === "fulfilled" ? { replies: result.value } : { error: result.reason instanceof Error ? result.reason.message : String(result.reason) }
|
|
61281
|
+
};
|
|
61282
|
+
});
|
|
61283
|
+
const totalFetches = footerRepliesResults.length + inlineRepliesResults.length;
|
|
61284
|
+
const failedFetches = [
|
|
61285
|
+
...footerRepliesResults,
|
|
61286
|
+
...inlineRepliesResults
|
|
61287
|
+
].filter((r) => r.status === "rejected").length;
|
|
61288
|
+
return toolResult(
|
|
61289
|
+
formatCommentThreads(fr, ir, page_id, failedFetches, totalFetches)
|
|
61290
|
+
);
|
|
60706
61291
|
}
|
|
60707
61292
|
return toolResult(formatComments(footerComments, inlineComments, page_id));
|
|
60708
61293
|
} catch (err) {
|
|
@@ -60756,7 +61341,7 @@ Color: ${state.color}` + echo
|
|
|
60756
61341
|
`Created ${type} comment ${comment2.id} on page ${page_id}` + echo
|
|
60757
61342
|
);
|
|
60758
61343
|
} catch (err) {
|
|
60759
|
-
return
|
|
61344
|
+
return toolErrorWithContext(err, { operation: "create_comment", resource: `page ${page_id}`, profile: config3.profile });
|
|
60760
61345
|
}
|
|
60761
61346
|
}
|
|
60762
61347
|
);
|
|
@@ -60785,7 +61370,7 @@ Color: ${state.color}` + echo
|
|
|
60785
61370
|
`Comment ${comment_id} ${state} (version: ${comment2.version?.number ?? "??"})` + echo
|
|
60786
61371
|
);
|
|
60787
61372
|
} catch (err) {
|
|
60788
|
-
return
|
|
61373
|
+
return toolErrorWithContext(err, { operation: "resolve_comment", resource: `comment ${comment_id}`, profile: config3.profile });
|
|
60789
61374
|
}
|
|
60790
61375
|
}
|
|
60791
61376
|
);
|
|
@@ -60815,7 +61400,7 @@ Color: ${state.color}` + echo
|
|
|
60815
61400
|
}
|
|
60816
61401
|
return toolResult(`Deleted ${type} comment ${comment_id}` + echo);
|
|
60817
61402
|
} catch (err) {
|
|
60818
|
-
return
|
|
61403
|
+
return toolErrorWithContext(err, { operation: "delete_comment", resource: `comment ${comment_id}`, profile: config3.profile });
|
|
60819
61404
|
}
|
|
60820
61405
|
}
|
|
60821
61406
|
);
|
|
@@ -61098,11 +61683,19 @@ ${sectionFenced}`
|
|
|
61098
61683
|
// E2: thread validated source for the mutation log.
|
|
61099
61684
|
source: effectiveSource
|
|
61100
61685
|
});
|
|
61686
|
+
const warnings = [];
|
|
61687
|
+
const labelResult = await ensureAttributionLabel(submitted.page.id);
|
|
61688
|
+
if (labelResult.warning) warnings.push(labelResult.warning);
|
|
61689
|
+
const badgeResult = await markPageUnverified(submitted.page.id, config3);
|
|
61690
|
+
if (badgeResult.warning) warnings.push(badgeResult.warning);
|
|
61101
61691
|
return toolResult(
|
|
61102
|
-
|
|
61692
|
+
appendWarnings(
|
|
61693
|
+
`Reverted: ${submitted.page.title} (ID: ${submitted.page.id}, v${target_version}\u2192v${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars)`,
|
|
61694
|
+
warnings
|
|
61695
|
+
) + echo
|
|
61103
61696
|
);
|
|
61104
61697
|
} catch (err) {
|
|
61105
|
-
return
|
|
61698
|
+
return toolErrorWithContext(err, { operation: "revert_page", resource: `page ${page_id}`, profile: config3.profile });
|
|
61106
61699
|
}
|
|
61107
61700
|
}
|
|
61108
61701
|
);
|
|
@@ -61191,7 +61784,7 @@ ${titleFenced}${echo2}`
|
|
|
61191
61784
|
inputSchema: {}
|
|
61192
61785
|
},
|
|
61193
61786
|
async () => {
|
|
61194
|
-
let text2 = `epimethian-mcp v${"6.0
|
|
61787
|
+
let text2 = `epimethian-mcp v${"6.1.0"}`;
|
|
61195
61788
|
try {
|
|
61196
61789
|
const pending = await getPendingUpdate();
|
|
61197
61790
|
if (pending) {
|
|
@@ -61222,7 +61815,7 @@ ${label} update available: v${pending.current} \u2192 v${pending.latest}. Run \`
|
|
|
61222
61815
|
const pending = await getPendingUpdate();
|
|
61223
61816
|
if (!pending) {
|
|
61224
61817
|
return toolResult(
|
|
61225
|
-
`epimethian-mcp v${"6.0
|
|
61818
|
+
`epimethian-mcp v${"6.1.0"} is already up to date.`
|
|
61226
61819
|
);
|
|
61227
61820
|
}
|
|
61228
61821
|
const output = await performUpgrade(pending.latest);
|
|
@@ -61244,7 +61837,7 @@ async function startRecoveryServer(profile) {
|
|
|
61244
61837
|
const server = new McpServer(
|
|
61245
61838
|
{
|
|
61246
61839
|
name: `confluence-${profile}-setup-needed`,
|
|
61247
|
-
version: "6.0
|
|
61840
|
+
version: "6.1.0"
|
|
61248
61841
|
},
|
|
61249
61842
|
{
|
|
61250
61843
|
instructions: `The Confluence profile "${profile}" referenced by CONFLUENCE_PROFILE has no keychain entry, so no Confluence tools are available. Call the setup_profile tool for instructions to create it.`
|
|
@@ -61295,21 +61888,21 @@ async function main() {
|
|
|
61295
61888
|
const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
|
|
61296
61889
|
const server = new McpServer({
|
|
61297
61890
|
name: serverName,
|
|
61298
|
-
version: "6.0
|
|
61891
|
+
version: "6.1.0"
|
|
61299
61892
|
});
|
|
61300
61893
|
await registerTools(server, config3);
|
|
61301
61894
|
const transport = new StdioServerTransport();
|
|
61302
61895
|
await server.connect(transport);
|
|
61303
61896
|
try {
|
|
61304
61897
|
const pending = await getPendingUpdate();
|
|
61305
|
-
if (pending && pending.current === "6.0
|
|
61898
|
+
if (pending && pending.current === "6.1.0") {
|
|
61306
61899
|
console.error(
|
|
61307
61900
|
`epimethian-mcp: update available: v${pending.current} \u2192 v${pending.latest} (${pending.type}). Run \`epimethian-mcp upgrade\` to install.`
|
|
61308
61901
|
);
|
|
61309
61902
|
}
|
|
61310
61903
|
} catch {
|
|
61311
61904
|
}
|
|
61312
|
-
checkForUpdates("6.0
|
|
61905
|
+
checkForUpdates("6.1.0").catch(() => {
|
|
61313
61906
|
});
|
|
61314
61907
|
}
|
|
61315
61908
|
|
|
@@ -61327,6 +61920,10 @@ async function run() {
|
|
|
61327
61920
|
} else if (command === "status") {
|
|
61328
61921
|
const { runStatus: runStatus2 } = await Promise.resolve().then(() => (init_status(), status_exports));
|
|
61329
61922
|
await runStatus2();
|
|
61923
|
+
} else if (command === "permissions") {
|
|
61924
|
+
const profile = process.argv[3];
|
|
61925
|
+
const { runPermissions: runPermissions2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
|
|
61926
|
+
await runPermissions2(profile);
|
|
61330
61927
|
} else if (command === "fix-legacy-links") {
|
|
61331
61928
|
const { runFixLegacyLinks: runFixLegacyLinks2 } = await Promise.resolve().then(() => (init_fix_legacy_links(), fix_legacy_links_exports));
|
|
61332
61929
|
await runFixLegacyLinks2(process.argv.slice(3));
|