@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/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.1"}`;
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.1"}`;
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(
@@ -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 args = process.argv.slice(2);
47731
- const explicitReadWrite = args.includes("--read-write");
47732
- let enableWrites = explicitReadWrite;
47733
- if (!explicitReadWrite && !args.includes("--read-only")) {
47734
- const rl2 = readline.createInterface({ input: import_node_process2.stdin, output: import_node_process2.stdout });
47735
- try {
47736
- const answer = await rl2.question("Enable writes for this profile? [y/N] ");
47737
- enableWrites = answer.trim().toLowerCase() === "y";
47738
- } finally {
47739
- 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
+ }
47740
48057
  }
47741
- }
47742
- const readOnly = !enableWrites;
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.1";
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
- 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
+ }
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
- 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
+ }
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
- 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);
59702
60228
  } catch (err) {
59703
- return toolError(err);
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)` + echo
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})` + echo
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 toolError(err);
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 toolError(err);
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})` + echo
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 toolError(err);
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
- 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);
60079
60620
  } catch (err) {
60080
- return toolError(err);
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
- 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);
60118
60664
  } catch (err) {
60119
- return toolError(err);
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 toolError(err);
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})` + echo
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 toolError(err);
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 toolError(err);
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 toolError(err);
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 toolError(err);
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 toolError(err);
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 [fr, ir] = await Promise.all([
60696
- 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 {
60697
61272
  comment: c,
60698
- replies: await getCommentReplies(c.id, "footer")
60699
- }))),
60700
- 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 {
60701
61279
  comment: c,
60702
- replies: await getCommentReplies(c.id, "inline")
60703
- })))
60704
- ]);
60705
- 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
+ );
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 toolError(err);
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 toolError(err);
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 toolError(err);
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
- `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
61103
61696
  );
61104
61697
  } catch (err) {
61105
- return toolError(err);
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.1"}`;
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.1"} is already up to date.`
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.1"
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.1"
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.1") {
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.1").catch(() => {
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));