@de-otio/epimethian-mcp 6.0.0 → 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/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 readOnly = registrySettings?.readOnly === true || process.env.CONFLUENCE_READ_ONLY === "true";
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
- throw new ConfluenceApiError(res.status, body);
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.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.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
- await addLabels(pageId, [ATTRIBUTION_LABEL]);
35452
- const labels = await getLabels(pageId);
35453
- if (labels.some((l) => l.name === LEGACY_ATTRIBUTION_LABEL)) {
35454
- await removeLabel(pageId, LEGACY_ATTRIBUTION_LABEL);
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(
@@ -46240,7 +46485,7 @@ function extractRawAcBlocks(md) {
46240
46485
  function escapeRegex2(s) {
46241
46486
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46242
46487
  }
46243
- function extractConfluenceSchemeLinks(md) {
46488
+ function extractConfluenceSchemeLinks(md, confluenceBaseUrl) {
46244
46489
  const links = /* @__PURE__ */ new Map();
46245
46490
  let idx = 0;
46246
46491
  const processed = md.replace(
@@ -46248,7 +46493,18 @@ function extractConfluenceSchemeLinks(md) {
46248
46493
  (_match, text2, href) => {
46249
46494
  const rest = href.slice("confluence://".length);
46250
46495
  const slashIdx = rest.indexOf("/");
46251
- if (slashIdx === -1) return _match;
46496
+ if (slashIdx === -1) {
46497
+ if (!/^\d+$/.test(rest)) return _match;
46498
+ if (!confluenceBaseUrl) {
46499
+ throw new ConverterError(
46500
+ `confluence://${rest} cannot be rewritten: no Confluence base URL is configured. Either configure one (the harness normally injects this), or use the confluence://SPACE_KEY/PAGE_TITLE form instead.`,
46501
+ "CONFLUENCE_LINK_NO_BASE_URL"
46502
+ );
46503
+ }
46504
+ const base = confluenceBaseUrl.replace(/\/+$/, "");
46505
+ const absoluteUrl = `${base}/wiki/pages/viewpage.action?pageId=${rest}`;
46506
+ return `[${text2}](${absoluteUrl})`;
46507
+ }
46252
46508
  const spaceKey = rest.slice(0, slashIdx);
46253
46509
  let pageTitle;
46254
46510
  try {
@@ -46386,7 +46642,7 @@ function markdownToStorage(md, opts) {
46386
46642
  mdi
46387
46643
  );
46388
46644
  const { processed: mdWithoutAcBlocks, blocks: acBlocks } = extractRawAcBlocks(mdWithoutColumns);
46389
- const { processed: mdWithoutConfluenceLinks, links: confluenceLinks } = extractConfluenceSchemeLinks(mdWithoutAcBlocks);
46645
+ const { processed: mdWithoutConfluenceLinks, links: confluenceLinks } = extractConfluenceSchemeLinks(mdWithoutAcBlocks, opts?.confluenceBaseUrl);
46390
46646
  let html;
46391
46647
  try {
46392
46648
  html = mdi.render(mdWithoutConfluenceLinks);
@@ -47364,6 +47620,62 @@ var init_safe_write = __esm({
47364
47620
  }
47365
47621
  });
47366
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
+
47367
47679
  // src/shared/update-check.ts
47368
47680
  function parseSemVer(version2) {
47369
47681
  const match2 = version2.match(/^(\d+)\.(\d+)\.(\d+)$/);
@@ -47716,23 +48028,39 @@ Profile will be saved without a tenant seal. Cross-tenant verification at startu
47716
48028
  );
47717
48029
  if (profile) {
47718
48030
  await addToProfileRegistry(profile);
47719
- const args = process.argv.slice(2);
47720
- const explicitReadWrite = args.includes("--read-write");
47721
- let enableWrites = explicitReadWrite;
47722
- if (!explicitReadWrite && !args.includes("--read-only")) {
47723
- const rl2 = readline.createInterface({ input: import_node_process2.stdin, output: import_node_process2.stdout });
47724
- try {
47725
- const answer = await rl2.question("Enable writes for this profile? [y/N] ");
47726
- enableWrites = answer.trim().toLowerCase() === "y";
47727
- } finally {
47728
- rl2.close();
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
+ }
47729
48057
  }
47730
- }
47731
- const readOnly = !enableWrites;
47732
- await setProfileSettings(profile, { readOnly });
47733
- const modeLabel = readOnly ? "read-only" : "read-write";
47734
- 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}).
47735
48060
  `);
48061
+ } finally {
48062
+ rl2.close();
48063
+ }
47736
48064
  } else {
47737
48065
  console.log("Credentials saved to OS keychain.\n");
47738
48066
  console.log(
@@ -48001,6 +48329,41 @@ var init_status = __esm({
48001
48329
  }
48002
48330
  });
48003
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
+
48004
48367
  // src/cli/fix-legacy-links.ts
48005
48368
  var fix_legacy_links_exports = {};
48006
48369
  __export(fix_legacy_links_exports, {
@@ -48313,7 +48676,7 @@ Informational:
48313
48676
  var install_agent_default;
48314
48677
  var init_install_agent = __esm({
48315
48678
  "install-agent.md"() {
48316
- 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';
48317
48680
  }
48318
48681
  });
48319
48682
 
@@ -48338,7 +48701,7 @@ __export(upgrade_exports, {
48338
48701
  runUpgrade: () => runUpgrade
48339
48702
  });
48340
48703
  async function runUpgrade() {
48341
- const currentVersion = "6.0.0";
48704
+ const currentVersion = "6.1.0";
48342
48705
  console.log(`epimethian-mcp upgrade: current version v${currentVersion}`);
48343
48706
  let pending = await getPendingUpdate();
48344
48707
  if (!pending) {
@@ -59151,6 +59514,81 @@ function storageToMarkdown(storage) {
59151
59514
 
59152
59515
  // src/server/index.ts
59153
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
59154
59592
  init_safe_write();
59155
59593
 
59156
59594
  // src/server/source-provenance.ts
@@ -59416,6 +59854,7 @@ async function assertSpaceAllowed(opts) {
59416
59854
  }
59417
59855
 
59418
59856
  // src/server/index.ts
59857
+ init_check_permissions();
59419
59858
  init_update_check();
59420
59859
  function getClientLabel(server) {
59421
59860
  const client = server.server.getClientVersion();
@@ -59470,7 +59909,16 @@ ${markdown}`;
59470
59909
 
59471
59910
  ${body}`;
59472
59911
  }
59912
+ var _sessionIsReadOnly = false;
59913
+ var _readOnlyNoteEmitted = false;
59473
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
+ }
59474
59922
  return { content: [{ type: "text", text: text2 }] };
59475
59923
  }
59476
59924
  function toolError(err) {
@@ -59478,6 +59926,42 @@ function toolError(err) {
59478
59926
  const message = sanitizeError(raw);
59479
59927
  return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
59480
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
+ }
59481
59965
  function tenantEcho(config3) {
59482
59966
  const host = new URL(config3.url).hostname;
59483
59967
  const mode = config3.profile ? `profile: ${config3.profile}` : "env-var mode";
@@ -59506,6 +59990,24 @@ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
59506
59990
  "lookup_user",
59507
59991
  "resolve_page_link"
59508
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
+ ]);
59509
60011
  function writeGuard(toolName, config3) {
59510
60012
  if (!config3.readOnly) return null;
59511
60013
  if (READ_ONLY_TOOLS.has(toolName)) return null;
@@ -59564,27 +60066,38 @@ function formatComments(footer, inline2, pageId) {
59564
60066
  }
59565
60067
  return lines.join("\n");
59566
60068
  }
59567
- function formatCommentThreads(footer, inline2, pageId) {
60069
+ function formatCommentThreads(footer, inline2, pageId, failedFetches = 0, totalFetches = 0) {
59568
60070
  const lines = [`Comments on page ${pageId}:`, ""];
59569
60071
  if (footer.length > 0) {
59570
60072
  lines.push(`Footer comments (${footer.length}):`);
59571
- footer.forEach(({ comment: comment2, replies }) => {
60073
+ footer.forEach(({ comment: comment2, replies, error: error2 }) => {
59572
60074
  lines.push(formatCommentLine(comment2));
59573
- replies.forEach((r) => lines.push(formatCommentLine(r, " ")));
60075
+ if (error2) {
60076
+ lines.push(` Error fetching replies: ${error2}`);
60077
+ } else if (replies) {
60078
+ replies.forEach((r) => lines.push(formatCommentLine(r, " ")));
60079
+ }
59574
60080
  });
59575
60081
  lines.push("");
59576
60082
  }
59577
60083
  if (inline2.length > 0) {
59578
60084
  lines.push(`Inline comments (${inline2.length}):`);
59579
- inline2.forEach(({ comment: comment2, replies }) => {
60085
+ inline2.forEach(({ comment: comment2, replies, error: error2 }) => {
59580
60086
  lines.push(formatCommentLine(comment2));
59581
- replies.forEach((r) => lines.push(formatCommentLine(r, " ")));
60087
+ if (error2) {
60088
+ lines.push(` Error fetching replies: ${error2}`);
60089
+ } else if (replies) {
60090
+ replies.forEach((r) => lines.push(formatCommentLine(r, " ")));
60091
+ }
59582
60092
  });
59583
60093
  lines.push("");
59584
60094
  }
59585
60095
  if (footer.length === 0 && inline2.length === 0) {
59586
60096
  lines.push("No comments found.");
59587
60097
  }
60098
+ if (failedFetches > 0 && totalFetches > 0) {
60099
+ lines.push(`Note: ${failedFetches} of ${totalFetches} reply fetches failed \u2014 partial results shown.`);
60100
+ }
59588
60101
  return lines.join("\n");
59589
60102
  }
59590
60103
  async function registerTools(server, config3) {
@@ -59593,11 +60106,30 @@ async function registerTools(server, config3) {
59593
60106
  const isToolEnabled = resolveToolFilter(settings);
59594
60107
  const allowedSpaces = settings?.spaces;
59595
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;
59596
60125
  const originalRegisterTool = server.registerTool.bind(server);
59597
60126
  server.registerTool = function(name, ...rest) {
59598
60127
  if (!isToolEnabled(name)) {
59599
60128
  return server;
59600
60129
  }
60130
+ if (isReadOnly && WRITE_TOOLS.has(name)) {
60131
+ return server;
60132
+ }
59601
60133
  return originalRegisterTool(name, ...rest);
59602
60134
  };
59603
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");
@@ -59687,9 +60219,14 @@ async function registerTools(server, config3) {
59687
60219
  deletedTokens: prepared.deletedTokens,
59688
60220
  clientLabel: getClientLabel(server)
59689
60221
  });
59690
- return toolResult(await formatPage(submitted.page, false) + echo);
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);
59691
60228
  } catch (err) {
59692
- return toolError(err);
60229
+ return toolErrorWithContext(err, { operation: "create_page", resource: `space ${space_key}`, profile: config3.profile });
59693
60230
  }
59694
60231
  }
59695
60232
  );
@@ -59884,17 +60421,22 @@ ${truncated}${truncationNote(origLen)}`
59884
60421
  source: effectiveSource
59885
60422
  });
59886
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);
59887
60429
  if (isTitleOnly) {
59888
60430
  return toolResult(
59889
- `Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, title only, body unchanged)` + echo
60431
+ appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, title only, body unchanged)`, warnings) + echo
59890
60432
  );
59891
60433
  }
59892
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(", ")}` : "";
59893
60435
  return toolResult(
59894
- `Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars${removalNote})` + echo
60436
+ appendWarnings(`Updated: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars${removalNote})`, warnings) + echo
59895
60437
  );
59896
60438
  } catch (err) {
59897
- return toolError(err);
60439
+ return toolErrorWithContext(err, { operation: "update_page", resource: `page ${page_id}`, profile: config3.profile });
59898
60440
  }
59899
60441
  }
59900
60442
  );
@@ -59956,7 +60498,7 @@ ${truncated}${truncationNote(origLen)}`
59956
60498
  return toolResult(`Deleted page ${page_id}` + echo);
59957
60499
  } catch (err) {
59958
60500
  logMutation(errorRecord("delete_page", page_id, err));
59959
- return toolError(err);
60501
+ return toolErrorWithContext(err, { operation: "delete_page", resource: `page ${page_id}`, profile: config3.profile });
59960
60502
  }
59961
60503
  }
59962
60504
  );
@@ -60022,12 +60564,17 @@ ${truncated}${truncationNote(origLen)}`
60022
60564
  operation: "update_page_section",
60023
60565
  clientLabel: getClientLabel(server)
60024
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);
60025
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(", ")}` : "";
60026
60573
  return toolResult(
60027
- `Updated section "${section}" in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})` + echo
60574
+ appendWarnings(`Updated section "${section}" in: ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion}${removalNote})`, warnings) + echo
60028
60575
  );
60029
60576
  } catch (err) {
60030
- return toolError(err);
60577
+ return toolErrorWithContext(err, { operation: "update_page_section", resource: `page ${page_id}`, profile: config3.profile });
60031
60578
  }
60032
60579
  }
60033
60580
  );
@@ -60064,9 +60611,14 @@ ${truncated}${truncationNote(origLen)}`
60064
60611
  "prepend",
60065
60612
  { separator, versionMessage: version_message ?? "Prepend content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url }
60066
60613
  );
60067
- return toolResult(`Prepended to: ${page.title} (ID: ${page.id}, version: ${newVersion}, body: ${oldLen}\u2192${newLen} chars)` + echo);
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);
60068
60620
  } catch (err) {
60069
- return toolError(err);
60621
+ return toolErrorWithContext(err, { operation: "prepend_to_page", resource: `page ${page_id}`, profile: config3.profile });
60070
60622
  }
60071
60623
  }
60072
60624
  );
@@ -60103,9 +60655,14 @@ ${truncated}${truncationNote(origLen)}`
60103
60655
  "append",
60104
60656
  { separator, versionMessage: version_message ?? "Append content", allowRawHtml: allow_raw_html, confluenceBaseUrl: confluence_base_url ?? cfg.url }
60105
60657
  );
60106
- return toolResult(`Appended to: ${page.title} (ID: ${page.id}, version: ${newVersion}, body: ${oldLen}\u2192${newLen} chars)` + echo);
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);
60107
60664
  } catch (err) {
60108
- return toolError(err);
60665
+ return toolErrorWithContext(err, { operation: "append_to_page", resource: `page ${page_id}`, profile: config3.profile });
60109
60666
  }
60110
60667
  }
60111
60668
  );
@@ -60228,6 +60785,23 @@ ${truncated}${truncationNote(origLen)}`
60228
60785
  }
60229
60786
  }
60230
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
+ );
60231
60805
  server.registerTool(
60232
60806
  "get_page_by_title",
60233
60807
  {
@@ -60363,7 +60937,7 @@ ${truncated}`);
60363
60937
  `Attached: ${att.title} (ID: ${att.id}, size: ${att.fileSize ?? "unknown"} bytes) to page ${page_id}` + echo
60364
60938
  );
60365
60939
  } catch (err) {
60366
- return toolError(err);
60940
+ return toolErrorWithContext(err, { operation: "add_attachment", resource: `page ${page_id}`, profile: config3.profile });
60367
60941
  }
60368
60942
  }
60369
60943
  );
@@ -60447,11 +61021,16 @@ ${macro}` : macro;
60447
61021
  clientLabel: getClientLabel(server),
60448
61022
  operation: "add_drawio_diagram"
60449
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);
60450
61029
  return toolResult(
60451
- `Diagram "${filename}" added to page ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion})` + echo
61030
+ appendWarnings(`Diagram "${filename}" added to page ${submitted.page.title} (ID: ${submitted.page.id}, version: ${submitted.newVersion})`, warnings) + echo
60452
61031
  );
60453
61032
  } catch (err) {
60454
- return toolError(err);
61033
+ return toolErrorWithContext(err, { operation: "add_drawio_diagram", resource: `page ${page_id}`, profile: config3.profile });
60455
61034
  }
60456
61035
  }
60457
61036
  );
@@ -60532,7 +61111,7 @@ ${lines}`);
60532
61111
  await addLabels(page_id, labels);
60533
61112
  return toolResult(`Added ${labels.length} label(s) to page ${page_id}: ${labels.join(", ")}` + echo);
60534
61113
  } catch (err) {
60535
- return toolError(err);
61114
+ return toolErrorWithContext(err, { operation: "add_label", resource: `page ${page_id}`, profile: config3.profile });
60536
61115
  }
60537
61116
  }
60538
61117
  );
@@ -60557,7 +61136,7 @@ ${lines}`);
60557
61136
  await removeLabel(page_id, label);
60558
61137
  return toolResult(`Removed label "${label}" from page ${page_id}` + echo);
60559
61138
  } catch (err) {
60560
- return toolError(err);
61139
+ return toolErrorWithContext(err, { operation: "remove_label", resource: `page ${page_id}`, profile: config3.profile });
60561
61140
  }
60562
61141
  }
60563
61142
  );
@@ -60630,7 +61209,7 @@ Color: ${state.color}` + echo
60630
61209
  await setContentState(page_id, name, color);
60631
61210
  return toolResult(`Set status on page ${page_id}: "${name}" (${color})` + echo);
60632
61211
  } catch (err) {
60633
- return toolError(err);
61212
+ return toolErrorWithContext(err, { operation: "set_page_status", resource: `page ${page_id}`, profile: config3.profile });
60634
61213
  }
60635
61214
  }
60636
61215
  );
@@ -60656,7 +61235,7 @@ Color: ${state.color}` + echo
60656
61235
  await removeContentState(page_id);
60657
61236
  return toolResult(`Removed status from page ${page_id}` + echo);
60658
61237
  } catch (err) {
60659
- return toolError(err);
61238
+ return toolErrorWithContext(err, { operation: "remove_page_status", resource: `page ${page_id}`, profile: config3.profile });
60660
61239
  }
60661
61240
  }
60662
61241
  );
@@ -60681,17 +61260,34 @@ Color: ${state.color}` + echo
60681
61260
  type !== "footer" ? getInlineComments(page_id, resolution_status) : Promise.resolve([])
60682
61261
  ]);
60683
61262
  if (include_replies) {
60684
- const [fr, ir] = await Promise.all([
60685
- Promise.all(footerComments.map(async (c) => ({
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 {
60686
61272
  comment: c,
60687
- replies: await getCommentReplies(c.id, "footer")
60688
- }))),
60689
- Promise.all(inlineComments.map(async (c) => ({
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 {
60690
61279
  comment: c,
60691
- replies: await getCommentReplies(c.id, "inline")
60692
- })))
60693
- ]);
60694
- return toolResult(formatCommentThreads(fr, ir, page_id));
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
+ );
60695
61291
  }
60696
61292
  return toolResult(formatComments(footerComments, inlineComments, page_id));
60697
61293
  } catch (err) {
@@ -60745,7 +61341,7 @@ Color: ${state.color}` + echo
60745
61341
  `Created ${type} comment ${comment2.id} on page ${page_id}` + echo
60746
61342
  );
60747
61343
  } catch (err) {
60748
- return toolError(err);
61344
+ return toolErrorWithContext(err, { operation: "create_comment", resource: `page ${page_id}`, profile: config3.profile });
60749
61345
  }
60750
61346
  }
60751
61347
  );
@@ -60774,7 +61370,7 @@ Color: ${state.color}` + echo
60774
61370
  `Comment ${comment_id} ${state} (version: ${comment2.version?.number ?? "??"})` + echo
60775
61371
  );
60776
61372
  } catch (err) {
60777
- return toolError(err);
61373
+ return toolErrorWithContext(err, { operation: "resolve_comment", resource: `comment ${comment_id}`, profile: config3.profile });
60778
61374
  }
60779
61375
  }
60780
61376
  );
@@ -60804,7 +61400,7 @@ Color: ${state.color}` + echo
60804
61400
  }
60805
61401
  return toolResult(`Deleted ${type} comment ${comment_id}` + echo);
60806
61402
  } catch (err) {
60807
- return toolError(err);
61403
+ return toolErrorWithContext(err, { operation: "delete_comment", resource: `comment ${comment_id}`, profile: config3.profile });
60808
61404
  }
60809
61405
  }
60810
61406
  );
@@ -61087,11 +61683,19 @@ ${sectionFenced}`
61087
61683
  // E2: thread validated source for the mutation log.
61088
61684
  source: effectiveSource
61089
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);
61090
61691
  return toolResult(
61091
- `Reverted: ${submitted.page.title} (ID: ${submitted.page.id}, v${target_version}\u2192v${submitted.newVersion}, body: ${submitted.oldLen}\u2192${submitted.newLen} chars)` + echo
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
61092
61696
  );
61093
61697
  } catch (err) {
61094
- return toolError(err);
61698
+ return toolErrorWithContext(err, { operation: "revert_page", resource: `page ${page_id}`, profile: config3.profile });
61095
61699
  }
61096
61700
  }
61097
61701
  );
@@ -61136,7 +61740,7 @@ ${lines.join("\n")}${echo2}`
61136
61740
  "resolve_page_link",
61137
61741
  {
61138
61742
  description: withUntrustedNote(
61139
- "Resolve a Confluence page to its stable content ID and URL given a page title and space key. Returns { contentId, url, spaceKey, title } for the matched page. Use this to obtain the contentId for <ac:link> page references via the confluence:// markdown scheme when authoring pages. Policy: if multiple pages share the same title in the space the first match is returned with a notice; use the exact page URL to disambiguate if needed."
61743
+ "Resolve a Confluence page to its stable content ID and URL given a page title and space key. Returns { contentId, url, spaceKey, title } for the matched page. When authoring pages, use the returned values to construct a confluence:// markdown link in either form: `[text](confluence://SPACE_KEY/PAGE_TITLE)` (preferred \u2014 produces an <ac:link> reference that follows the page across renames) or `[text](confluence://CONTENT_ID)` (produces a plain anchor to the page's stable URL). Policy: if multiple pages share the same title in the space the first match is returned with a notice; use the exact page URL to disambiguate if needed."
61140
61744
  ),
61141
61745
  inputSchema: {
61142
61746
  title: external_exports.string().min(1).describe("Exact page title to look up."),
@@ -61180,7 +61784,7 @@ ${titleFenced}${echo2}`
61180
61784
  inputSchema: {}
61181
61785
  },
61182
61786
  async () => {
61183
- let text2 = `epimethian-mcp v${"6.0.0"}`;
61787
+ let text2 = `epimethian-mcp v${"6.1.0"}`;
61184
61788
  try {
61185
61789
  const pending = await getPendingUpdate();
61186
61790
  if (pending) {
@@ -61211,7 +61815,7 @@ ${label} update available: v${pending.current} \u2192 v${pending.latest}. Run \`
61211
61815
  const pending = await getPendingUpdate();
61212
61816
  if (!pending) {
61213
61817
  return toolResult(
61214
- `epimethian-mcp v${"6.0.0"} is already up to date.`
61818
+ `epimethian-mcp v${"6.1.0"} is already up to date.`
61215
61819
  );
61216
61820
  }
61217
61821
  const output = await performUpgrade(pending.latest);
@@ -61233,7 +61837,7 @@ async function startRecoveryServer(profile) {
61233
61837
  const server = new McpServer(
61234
61838
  {
61235
61839
  name: `confluence-${profile}-setup-needed`,
61236
- version: "6.0.0"
61840
+ version: "6.1.0"
61237
61841
  },
61238
61842
  {
61239
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.`
@@ -61284,21 +61888,21 @@ async function main() {
61284
61888
  const serverName = config3.profile ? `confluence-${config3.profile}` : "confluence";
61285
61889
  const server = new McpServer({
61286
61890
  name: serverName,
61287
- version: "6.0.0"
61891
+ version: "6.1.0"
61288
61892
  });
61289
61893
  await registerTools(server, config3);
61290
61894
  const transport = new StdioServerTransport();
61291
61895
  await server.connect(transport);
61292
61896
  try {
61293
61897
  const pending = await getPendingUpdate();
61294
- if (pending && pending.current === "6.0.0") {
61898
+ if (pending && pending.current === "6.1.0") {
61295
61899
  console.error(
61296
61900
  `epimethian-mcp: update available: v${pending.current} \u2192 v${pending.latest} (${pending.type}). Run \`epimethian-mcp upgrade\` to install.`
61297
61901
  );
61298
61902
  }
61299
61903
  } catch {
61300
61904
  }
61301
- checkForUpdates("6.0.0").catch(() => {
61905
+ checkForUpdates("6.1.0").catch(() => {
61302
61906
  });
61303
61907
  }
61304
61908
 
@@ -61316,6 +61920,10 @@ async function run() {
61316
61920
  } else if (command === "status") {
61317
61921
  const { runStatus: runStatus2 } = await Promise.resolve().then(() => (init_status(), status_exports));
61318
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);
61319
61927
  } else if (command === "fix-legacy-links") {
61320
61928
  const { runFixLegacyLinks: runFixLegacyLinks2 } = await Promise.resolve().then(() => (init_fix_legacy_links(), fix_legacy_links_exports));
61321
61929
  await runFixLegacyLinks2(process.argv.slice(3));