@de-otio/epimethian-mcp 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -6803,20 +6803,28 @@ var require_dist = __commonJS({
6803
6803
  });
6804
6804
 
6805
6805
  // src/shared/keychain.ts
6806
+ function accountForProfile(profile) {
6807
+ if (!PROFILE_NAME_RE.test(profile)) {
6808
+ throw new Error(
6809
+ `Invalid profile name: "${profile}". Use lowercase alphanumeric and hyphens only (1-63 chars).`
6810
+ );
6811
+ }
6812
+ return `${LEGACY_ACCOUNT}/${profile}`;
6813
+ }
6806
6814
  function exec(cmd, args) {
6807
6815
  return new Promise((resolve2, reject) => {
6808
- (0, import_node_child_process.execFile)(cmd, args, { timeout: 5e3 }, (err, stdout2, stderr) => {
6816
+ (0, import_node_child_process.execFile)(cmd, args, { timeout: 5e3 }, (err, stdout3, stderr) => {
6809
6817
  if (err) {
6810
6818
  reject(new Error(stderr || err.message));
6811
6819
  } else {
6812
- resolve2(stdout2);
6820
+ resolve2(stdout3);
6813
6821
  }
6814
6822
  });
6815
6823
  });
6816
6824
  }
6817
- async function writeMacOS(password) {
6825
+ async function writeMacOS(account, password) {
6818
6826
  try {
6819
- await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", ACCOUNT]);
6827
+ await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", account]);
6820
6828
  } catch {
6821
6829
  }
6822
6830
  await exec("security", [
@@ -6824,23 +6832,26 @@ async function writeMacOS(password) {
6824
6832
  "-s",
6825
6833
  SERVICE,
6826
6834
  "-a",
6827
- ACCOUNT,
6835
+ account,
6828
6836
  "-w",
6829
6837
  password,
6830
6838
  "-U"
6831
6839
  ]);
6832
6840
  }
6833
- async function readMacOS() {
6841
+ async function readMacOS(account) {
6834
6842
  return (await exec("security", [
6835
6843
  "find-generic-password",
6836
6844
  "-s",
6837
6845
  SERVICE,
6838
6846
  "-a",
6839
- ACCOUNT,
6847
+ account,
6840
6848
  "-w"
6841
6849
  ])).trim();
6842
6850
  }
6843
- async function writeLinux(password) {
6851
+ async function deleteMacOS(account) {
6852
+ await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", account]);
6853
+ }
6854
+ async function writeLinux(account, password) {
6844
6855
  return new Promise((resolve2, reject) => {
6845
6856
  const proc = (0, import_node_child_process.spawn)("secret-tool", [
6846
6857
  "store",
@@ -6849,7 +6860,7 @@ async function writeLinux(password) {
6849
6860
  "service",
6850
6861
  SERVICE,
6851
6862
  "account",
6852
- ACCOUNT
6863
+ account
6853
6864
  ]);
6854
6865
  proc.stdin.write(password);
6855
6866
  proc.stdin.end();
@@ -6860,55 +6871,122 @@ async function writeLinux(password) {
6860
6871
  proc.on("error", reject);
6861
6872
  });
6862
6873
  }
6863
- async function readLinux() {
6874
+ async function readLinux(account) {
6864
6875
  return (await exec("secret-tool", [
6865
6876
  "lookup",
6866
6877
  "service",
6867
6878
  SERVICE,
6868
6879
  "account",
6869
- ACCOUNT
6880
+ account
6870
6881
  ])).trim();
6871
6882
  }
6872
- async function saveToKeychain(creds) {
6883
+ async function deleteLinux(account) {
6884
+ await exec("secret-tool", [
6885
+ "clear",
6886
+ "service",
6887
+ SERVICE,
6888
+ "account",
6889
+ account
6890
+ ]);
6891
+ }
6892
+ function resolveAccount(profile) {
6893
+ if (profile !== void 0) {
6894
+ return accountForProfile(profile);
6895
+ }
6896
+ return LEGACY_ACCOUNT;
6897
+ }
6898
+ async function saveToKeychain(creds, profile) {
6899
+ const account = resolveAccount(profile);
6873
6900
  const json = JSON.stringify(creds);
6874
6901
  if (process.platform === "darwin") {
6875
- await writeMacOS(json);
6902
+ await writeMacOS(account, json);
6876
6903
  } else if (process.platform === "linux") {
6877
- await writeLinux(json);
6904
+ await writeLinux(account, json);
6878
6905
  } else {
6879
6906
  throw new Error(`Keychain not supported on ${process.platform}`);
6880
6907
  }
6881
6908
  }
6882
- async function readFromKeychain() {
6909
+ async function readFromKeychain(profile) {
6910
+ const account = resolveAccount(profile);
6911
+ let raw;
6883
6912
  try {
6884
- let raw;
6885
6913
  if (process.platform === "darwin") {
6886
- raw = await readMacOS();
6914
+ raw = await readMacOS(account);
6887
6915
  } else if (process.platform === "linux") {
6888
- raw = await readLinux();
6916
+ raw = await readLinux(account);
6889
6917
  } else {
6890
6918
  return null;
6891
6919
  }
6892
- const parsed = JSON.parse(raw);
6893
- if (parsed && typeof parsed.apiToken === "string") {
6894
- return parsed;
6895
- }
6896
- return null;
6897
6920
  } catch {
6898
6921
  return null;
6899
6922
  }
6923
+ let parsed;
6924
+ try {
6925
+ parsed = JSON.parse(raw);
6926
+ } catch {
6927
+ const label = profile ? `profile "${profile}"` : "legacy keychain entry";
6928
+ throw new Error(`Corrupted keychain entry for ${label}: invalid JSON.`);
6929
+ }
6930
+ if (!parsed || typeof parsed !== "object" || typeof parsed.apiToken !== "string" || typeof parsed.url !== "string" || typeof parsed.email !== "string") {
6931
+ const label = profile ? `profile "${profile}"` : "legacy keychain entry";
6932
+ throw new Error(
6933
+ `Corrupted keychain entry for ${label}: missing required fields (url, email, apiToken).`
6934
+ );
6935
+ }
6936
+ return parsed;
6937
+ }
6938
+ async function deleteFromKeychain(profile) {
6939
+ const account = resolveAccount(profile);
6940
+ try {
6941
+ if (process.platform === "darwin") {
6942
+ await deleteMacOS(account);
6943
+ } else if (process.platform === "linux") {
6944
+ await deleteLinux(account);
6945
+ }
6946
+ } catch {
6947
+ }
6900
6948
  }
6901
- var import_node_child_process, SERVICE, ACCOUNT;
6949
+ var import_node_child_process, SERVICE, LEGACY_ACCOUNT, PROFILE_NAME_RE;
6902
6950
  var init_keychain = __esm({
6903
6951
  "src/shared/keychain.ts"() {
6904
6952
  "use strict";
6905
6953
  import_node_child_process = require("node:child_process");
6906
6954
  SERVICE = "epimethian-mcp";
6907
- ACCOUNT = "confluence-credentials";
6955
+ LEGACY_ACCOUNT = "confluence-credentials";
6956
+ PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
6908
6957
  }
6909
6958
  });
6910
6959
 
6911
6960
  // src/shared/test-connection.ts
6961
+ async function verifyTenantIdentity(url, email2, apiToken) {
6962
+ const endpoint = `${url.replace(/\/+$/, "")}/wiki/rest/api/user/current`;
6963
+ const auth = Buffer.from(`${email2}:${apiToken}`).toString("base64");
6964
+ try {
6965
+ const response = await fetch(endpoint, {
6966
+ method: "GET",
6967
+ headers: {
6968
+ Authorization: `Basic ${auth}`,
6969
+ Accept: "application/json"
6970
+ }
6971
+ });
6972
+ if (!response.ok) {
6973
+ return { ok: false, message: `HTTP ${response.status}: ${response.statusText}` };
6974
+ }
6975
+ const body = await response.json();
6976
+ const authenticatedEmail = body.email ?? "";
6977
+ if (authenticatedEmail.toLowerCase() !== email2.toLowerCase()) {
6978
+ return {
6979
+ ok: false,
6980
+ authenticatedEmail,
6981
+ message: `Tenant identity mismatch. Expected: ${email2}, authenticated as: ${authenticatedEmail}. This may indicate a DNS or configuration issue.`
6982
+ };
6983
+ }
6984
+ return { ok: true, authenticatedEmail, message: `Verified identity: ${authenticatedEmail}` };
6985
+ } catch (err) {
6986
+ const message = err instanceof Error ? err.message : String(err);
6987
+ return { ok: false, message: `Identity verification failed: ${message}` };
6988
+ }
6989
+ }
6912
6990
  async function testConnection(url, email2, apiToken) {
6913
6991
  const endpoint = `${url.replace(/\/+$/, "")}/wiki/api/v2/spaces?limit=1`;
6914
6992
  const auth = Buffer.from(`${email2}:${apiToken}`).toString("base64");
@@ -6946,6 +7024,76 @@ var init_test_connection = __esm({
6946
7024
  }
6947
7025
  });
6948
7026
 
7027
+ // src/shared/profiles.ts
7028
+ async function ensureConfigDir() {
7029
+ await (0, import_promises2.mkdir)(CONFIG_DIR, { recursive: true, mode: 448 });
7030
+ }
7031
+ async function readProfileRegistry() {
7032
+ try {
7033
+ const raw = await (0, import_promises2.readFile)(REGISTRY_FILE, "utf-8");
7034
+ const parsed = JSON.parse(raw);
7035
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.profiles) && parsed.profiles.every((p) => typeof p === "string")) {
7036
+ return parsed.profiles;
7037
+ }
7038
+ console.error(
7039
+ "Warning: Profile registry has unexpected format. Treating as empty."
7040
+ );
7041
+ return [];
7042
+ } catch (err) {
7043
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") {
7044
+ return [];
7045
+ }
7046
+ console.error("Warning: Could not read profile registry. Treating as empty.");
7047
+ return [];
7048
+ }
7049
+ }
7050
+ async function addToProfileRegistry(name) {
7051
+ await ensureConfigDir();
7052
+ const profiles = await readProfileRegistry();
7053
+ if (profiles.includes(name)) return;
7054
+ profiles.push(name);
7055
+ const data = JSON.stringify({ profiles }, null, 2) + "\n";
7056
+ const tmpFile = (0, import_node_path2.join)(
7057
+ CONFIG_DIR,
7058
+ `.profiles.${(0, import_node_crypto.randomBytes)(4).toString("hex")}.tmp`
7059
+ );
7060
+ await (0, import_promises2.writeFile)(tmpFile, data, { mode: 384 });
7061
+ await (0, import_promises2.rename)(tmpFile, REGISTRY_FILE);
7062
+ }
7063
+ async function removeFromProfileRegistry(name) {
7064
+ const profiles = await readProfileRegistry();
7065
+ const filtered = profiles.filter((p) => p !== name);
7066
+ if (filtered.length === profiles.length) return;
7067
+ await ensureConfigDir();
7068
+ const data = JSON.stringify({ profiles: filtered }, null, 2) + "\n";
7069
+ const tmpFile = (0, import_node_path2.join)(
7070
+ CONFIG_DIR,
7071
+ `.profiles.${(0, import_node_crypto.randomBytes)(4).toString("hex")}.tmp`
7072
+ );
7073
+ await (0, import_promises2.writeFile)(tmpFile, data, { mode: 384 });
7074
+ await (0, import_promises2.rename)(tmpFile, REGISTRY_FILE);
7075
+ }
7076
+ async function appendAuditLog(message) {
7077
+ await ensureConfigDir();
7078
+ const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}
7079
+ `;
7080
+ const { appendFile } = await import("node:fs/promises");
7081
+ await appendFile(AUDIT_LOG, entry, { mode: 384 });
7082
+ }
7083
+ var import_promises2, import_node_path2, import_node_os2, import_node_crypto, CONFIG_DIR, REGISTRY_FILE, AUDIT_LOG;
7084
+ var init_profiles = __esm({
7085
+ "src/shared/profiles.ts"() {
7086
+ "use strict";
7087
+ import_promises2 = require("node:fs/promises");
7088
+ import_node_path2 = require("node:path");
7089
+ import_node_os2 = require("node:os");
7090
+ import_node_crypto = require("node:crypto");
7091
+ CONFIG_DIR = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".config", "epimethian-mcp");
7092
+ REGISTRY_FILE = (0, import_node_path2.join)(CONFIG_DIR, "profiles.json");
7093
+ AUDIT_LOG = (0, import_node_path2.join)(CONFIG_DIR, "audit.log");
7094
+ }
7095
+ });
7096
+
6949
7097
  // src/cli/setup.ts
6950
7098
  var setup_exports = {};
6951
7099
  __export(setup_exports, {
@@ -6981,15 +7129,22 @@ function readPassword(prompt) {
6981
7129
  import_node_process2.stdin.on("data", onData);
6982
7130
  });
6983
7131
  }
6984
- async function runSetup() {
7132
+ async function runSetup(profile) {
6985
7133
  if (!import_node_process2.stdin.isTTY) {
6986
7134
  console.error(
6987
7135
  "Error: setup requires an interactive terminal.\nFor non-interactive environments, set CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_API_TOKEN as environment variables."
6988
7136
  );
6989
7137
  process.exit(1);
6990
7138
  }
6991
- console.log("Epimethian MCP - Confluence credential setup\n");
6992
- const existing = await readFromKeychain();
7139
+ if (profile !== void 0 && !PROFILE_NAME_RE.test(profile)) {
7140
+ console.error(
7141
+ `Error: Invalid profile name "${profile}". Use lowercase alphanumeric and hyphens only (1-63 chars).`
7142
+ );
7143
+ process.exit(1);
7144
+ }
7145
+ const banner = profile ? `Epimethian MCP - Credential setup for profile "${profile}"` : "Epimethian MCP - Confluence credential setup";
7146
+ console.log(banner + "\n");
7147
+ const existing = await readFromKeychain(profile);
6993
7148
  const rl = readline.createInterface({ input: import_node_process2.stdin, output: import_node_process2.stdout });
6994
7149
  try {
6995
7150
  const defaultUrl = existing?.url ?? "";
@@ -7005,6 +7160,21 @@ async function runSetup() {
7005
7160
  console.error("Error: URL must start with https://");
7006
7161
  process.exit(1);
7007
7162
  }
7163
+ try {
7164
+ const parsed = new URL(url);
7165
+ if (parsed.username || parsed.password || /[\n\r]/.test(url)) {
7166
+ console.error("Error: URL contains invalid characters.");
7167
+ process.exit(1);
7168
+ }
7169
+ if (!parsed.hostname.endsWith(".atlassian.net")) {
7170
+ console.error(
7171
+ `Warning: URL does not match *.atlassian.net. Ensure this is the correct Confluence instance.`
7172
+ );
7173
+ }
7174
+ } catch {
7175
+ console.error("Error: Invalid URL format.");
7176
+ process.exit(1);
7177
+ }
7008
7178
  const defaultEmail = existing?.email ?? "";
7009
7179
  const emailPrompt = defaultEmail ? `Email [${defaultEmail}]: ` : "Email: ";
7010
7180
  let email2 = (await rl.question(emailPrompt)).trim();
@@ -7029,8 +7199,17 @@ async function runSetup() {
7029
7199
  }
7030
7200
  console.log(`${result.message}
7031
7201
  `);
7032
- await saveToKeychain({ url, email: email2, apiToken });
7033
- console.log("Credentials saved to OS keychain.\n");
7202
+ await saveToKeychain({ url, email: email2, apiToken }, profile);
7203
+ if (profile) {
7204
+ await addToProfileRegistry(profile);
7205
+ console.log(`Credentials saved to OS keychain (profile: ${profile}).
7206
+ `);
7207
+ } else {
7208
+ console.log("Credentials saved to OS keychain.\n");
7209
+ console.log(
7210
+ "Tip: Use --profile <name> for multi-tenant support.\n"
7211
+ );
7212
+ }
7034
7213
  console.log(`Available tools (${TOOLS.length}):`);
7035
7214
  console.log(` ${TOOLS.join(", ")}`);
7036
7215
  console.log(
@@ -7048,6 +7227,7 @@ var init_setup = __esm({
7048
7227
  import_node_process2 = require("node:process");
7049
7228
  init_test_connection();
7050
7229
  init_keychain();
7230
+ init_profiles();
7051
7231
  TOOLS = [
7052
7232
  "create_page",
7053
7233
  "get_page",
@@ -7065,6 +7245,164 @@ var init_setup = __esm({
7065
7245
  }
7066
7246
  });
7067
7247
 
7248
+ // src/cli/profiles.ts
7249
+ var profiles_exports = {};
7250
+ __export(profiles_exports, {
7251
+ runProfiles: () => runProfiles
7252
+ });
7253
+ async function runProfiles() {
7254
+ const args = process.argv.slice(3);
7255
+ const removeIdx = args.indexOf("--remove");
7256
+ if (removeIdx > -1) {
7257
+ const name = args[removeIdx + 1];
7258
+ if (!name || !PROFILE_NAME_RE.test(name)) {
7259
+ console.error(
7260
+ "Error: --remove requires a valid profile name."
7261
+ );
7262
+ process.exit(1);
7263
+ }
7264
+ await removeProfile(name, args.includes("--force"));
7265
+ return;
7266
+ }
7267
+ const verbose = args.includes("--verbose");
7268
+ const profiles = await readProfileRegistry();
7269
+ if (profiles.length === 0) {
7270
+ console.log("No profiles configured. Run `epimethian-mcp setup --profile <name>` to create one.");
7271
+ return;
7272
+ }
7273
+ if (verbose) {
7274
+ console.log(
7275
+ ` ${"Profile".padEnd(20)} ${"URL".padEnd(40)} Email`
7276
+ );
7277
+ console.log(
7278
+ ` ${"\u2500".repeat(20)} ${"\u2500".repeat(40)} ${"\u2500".repeat(30)}`
7279
+ );
7280
+ for (const name of profiles) {
7281
+ try {
7282
+ const creds = await readFromKeychain(name);
7283
+ if (creds) {
7284
+ console.log(
7285
+ ` ${name.padEnd(20)} ${creds.url.padEnd(40)} ${creds.email}`
7286
+ );
7287
+ } else {
7288
+ console.log(
7289
+ ` ${name.padEnd(20)} (credentials missing)`
7290
+ );
7291
+ }
7292
+ } catch {
7293
+ console.log(
7294
+ ` ${name.padEnd(20)} (credentials corrupted)`
7295
+ );
7296
+ }
7297
+ }
7298
+ } else {
7299
+ console.log("Configured profiles:");
7300
+ for (const name of profiles) {
7301
+ console.log(` ${name}`);
7302
+ }
7303
+ console.log("\nUse --verbose to show URLs and emails.");
7304
+ }
7305
+ }
7306
+ async function removeProfile(name, force) {
7307
+ if (!force || import_node_process3.stdin.isTTY) {
7308
+ if (!import_node_process3.stdin.isTTY) {
7309
+ console.error(
7310
+ "Error: Removing a profile requires an interactive terminal or --force in non-TTY mode."
7311
+ );
7312
+ process.exit(1);
7313
+ }
7314
+ const rl = readline2.createInterface({ input: import_node_process3.stdin, output: import_node_process3.stdout });
7315
+ try {
7316
+ const answer = await rl.question(
7317
+ `Remove profile "${name}" and delete its credentials? [y/N] `
7318
+ );
7319
+ if (answer.trim().toLowerCase() !== "y") {
7320
+ console.log("Cancelled.");
7321
+ return;
7322
+ }
7323
+ } finally {
7324
+ rl.close();
7325
+ }
7326
+ }
7327
+ await deleteFromKeychain(name);
7328
+ await removeFromProfileRegistry(name);
7329
+ await appendAuditLog(`Removed profile "${name}"`);
7330
+ console.log(`Profile "${name}" removed.`);
7331
+ }
7332
+ var import_node_process3, readline2;
7333
+ var init_profiles2 = __esm({
7334
+ "src/cli/profiles.ts"() {
7335
+ "use strict";
7336
+ import_node_process3 = require("node:process");
7337
+ readline2 = __toESM(require("node:readline/promises"));
7338
+ init_keychain();
7339
+ init_profiles();
7340
+ }
7341
+ });
7342
+
7343
+ // src/cli/status.ts
7344
+ var status_exports = {};
7345
+ __export(status_exports, {
7346
+ runStatus: () => runStatus
7347
+ });
7348
+ async function runStatus() {
7349
+ const profile = process.env.CONFLUENCE_PROFILE || "";
7350
+ const urlEnv = process.env.CONFLUENCE_URL || "";
7351
+ const emailEnv = process.env.CONFLUENCE_EMAIL || "";
7352
+ const tokenEnv = process.env.CONFLUENCE_API_TOKEN || "";
7353
+ let url;
7354
+ let email2;
7355
+ let apiToken;
7356
+ let mode;
7357
+ if (profile) {
7358
+ if (!PROFILE_NAME_RE.test(profile)) {
7359
+ console.error(`Invalid CONFLUENCE_PROFILE: "${profile}".`);
7360
+ process.exit(1);
7361
+ }
7362
+ const creds = await readFromKeychain(profile);
7363
+ if (!creds) {
7364
+ console.error(
7365
+ `No credentials found for profile "${profile}". Run \`epimethian-mcp setup --profile ${profile}\` to configure.`
7366
+ );
7367
+ process.exit(1);
7368
+ }
7369
+ url = creds.url;
7370
+ email2 = creds.email;
7371
+ apiToken = creds.apiToken;
7372
+ mode = `profile: ${profile}`;
7373
+ } else if (urlEnv && emailEnv && tokenEnv) {
7374
+ url = urlEnv;
7375
+ email2 = emailEnv;
7376
+ apiToken = tokenEnv;
7377
+ mode = "env-var mode";
7378
+ } else {
7379
+ const legacy = await readFromKeychain();
7380
+ if (!legacy) {
7381
+ console.error(
7382
+ "No credentials configured. Run `epimethian-mcp setup --profile <name>` to get started."
7383
+ );
7384
+ process.exit(1);
7385
+ }
7386
+ url = legacy.url;
7387
+ email2 = legacy.email;
7388
+ apiToken = legacy.apiToken;
7389
+ mode = "legacy keychain (no profile)";
7390
+ }
7391
+ console.log(`Profile: ${mode}`);
7392
+ console.log(`URL: ${url}`);
7393
+ console.log(`Email: ${email2}`);
7394
+ console.log("Testing connection...");
7395
+ const result = await testConnection(url, email2, apiToken);
7396
+ console.log(`Status: ${result.ok ? "Connected" : "Failed"} - ${result.message}`);
7397
+ }
7398
+ var init_status = __esm({
7399
+ "src/cli/status.ts"() {
7400
+ "use strict";
7401
+ init_test_connection();
7402
+ init_keychain();
7403
+ }
7404
+ });
7405
+
7068
7406
  // node_modules/zod/v3/external.js
7069
7407
  var external_exports = {};
7070
7408
  __export(external_exports, {
@@ -21285,29 +21623,64 @@ var import_node_path = require("node:path");
21285
21623
 
21286
21624
  // src/server/confluence-client.ts
21287
21625
  init_keychain();
21626
+ init_test_connection();
21288
21627
  var _config = null;
21289
- async function getConfig() {
21290
- if (_config) return _config;
21291
- let url = process.env.CONFLUENCE_URL?.replace(/\/$/, "") || "";
21292
- let email2 = process.env.CONFLUENCE_EMAIL || "";
21293
- let apiToken = process.env.CONFLUENCE_API_TOKEN || "";
21294
- if (!url || !email2 || !apiToken) {
21295
- const keychainCreds = await readFromKeychain();
21296
- if (keychainCreds) {
21297
- url = url || keychainCreds.url.replace(/\/$/, "");
21298
- email2 = email2 || keychainCreds.email;
21299
- apiToken = apiToken || keychainCreds.apiToken;
21300
- }
21301
- }
21302
- if (!url || !email2 || !apiToken) {
21628
+ async function resolveCredentials() {
21629
+ const profileEnv = process.env.CONFLUENCE_PROFILE || "";
21630
+ const urlEnv = process.env.CONFLUENCE_URL?.replace(/\/$/, "") || "";
21631
+ const emailEnv = process.env.CONFLUENCE_EMAIL || "";
21632
+ const tokenEnv = process.env.CONFLUENCE_API_TOKEN || "";
21633
+ if (profileEnv) {
21634
+ if (!PROFILE_NAME_RE.test(profileEnv)) {
21635
+ console.error(
21636
+ `Invalid CONFLUENCE_PROFILE: "${profileEnv}". Use lowercase alphanumeric and hyphens only (1-63 chars).`
21637
+ );
21638
+ process.exit(1);
21639
+ }
21640
+ const creds = await readFromKeychain(profileEnv);
21641
+ if (!creds) {
21642
+ console.error(
21643
+ `No credentials found for profile "${profileEnv}". Run \`epimethian-mcp setup --profile ${profileEnv}\` to configure.`
21644
+ );
21645
+ process.exit(1);
21646
+ }
21647
+ return {
21648
+ url: creds.url.replace(/\/$/, ""),
21649
+ email: creds.email,
21650
+ apiToken: creds.apiToken,
21651
+ profile: profileEnv
21652
+ };
21653
+ }
21654
+ if (urlEnv && emailEnv && tokenEnv) {
21655
+ delete process.env.CONFLUENCE_API_TOKEN;
21656
+ return { url: urlEnv, email: emailEnv, apiToken: tokenEnv, profile: null };
21657
+ }
21658
+ const setVars = [
21659
+ urlEnv && "CONFLUENCE_URL",
21660
+ emailEnv && "CONFLUENCE_EMAIL",
21661
+ tokenEnv && "CONFLUENCE_API_TOKEN"
21662
+ ].filter(Boolean);
21663
+ if (setVars.length > 0) {
21303
21664
  console.error(
21304
- "Missing Confluence credentials. Set CONFLUENCE_URL, CONFLUENCE_EMAIL, CONFLUENCE_API_TOKEN environment variables, or run `epimethian-mcp setup`."
21665
+ `Error: Partial credentials detected (${setVars.join(", ")} set, but not all three).
21666
+ Either set CONFLUENCE_PROFILE or provide all three environment variables (CONFLUENCE_URL, CONFLUENCE_EMAIL, CONFLUENCE_API_TOKEN).
21667
+ Run \`epimethian-mcp setup --profile <name>\` for guided setup.`
21305
21668
  );
21306
21669
  process.exit(1);
21307
21670
  }
21671
+ console.error(
21672
+ "Missing Confluence credentials. Set CONFLUENCE_PROFILE environment variable, or run `epimethian-mcp setup --profile <name>` to configure."
21673
+ );
21674
+ process.exit(1);
21675
+ }
21676
+ async function getConfig() {
21677
+ if (_config) return _config;
21678
+ const { url, email: email2, apiToken, profile } = await resolveCredentials();
21308
21679
  const authHeader = "Basic " + Buffer.from(`${email2}:${apiToken}`).toString("base64");
21309
- _config = {
21680
+ _config = Object.freeze({
21310
21681
  url,
21682
+ email: email2,
21683
+ profile,
21311
21684
  apiV2: `${url}/wiki/api/v2`,
21312
21685
  apiV1: `${url}/wiki/rest/api`,
21313
21686
  authHeader,
@@ -21315,9 +21688,43 @@ async function getConfig() {
21315
21688
  Authorization: authHeader,
21316
21689
  "Content-Type": "application/json"
21317
21690
  }
21318
- };
21691
+ });
21319
21692
  return _config;
21320
21693
  }
21694
+ async function validateStartup(config2) {
21695
+ const { url, email: email2, profile } = config2;
21696
+ const decoded = Buffer.from(
21697
+ config2.authHeader.replace("Basic ", ""),
21698
+ "base64"
21699
+ ).toString();
21700
+ const colonIndex = decoded.indexOf(":");
21701
+ const apiToken = decoded.slice(colonIndex + 1);
21702
+ const connResult = await testConnection(url, email2, apiToken);
21703
+ if (!connResult.ok) {
21704
+ const profileHint = profile ? `Run \`epimethian-mcp setup --profile ${profile}\` to update credentials.` : "Check your CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_API_TOKEN.";
21705
+ console.error(
21706
+ `Error: Confluence credentials rejected by ${url}
21707
+ ${connResult.message}
21708
+ ${profileHint}`
21709
+ );
21710
+ process.exit(1);
21711
+ }
21712
+ const identityResult = await verifyTenantIdentity(url, email2, apiToken);
21713
+ if (!identityResult.ok) {
21714
+ const profileHint = profile ? `Run \`epimethian-mcp setup --profile ${profile}\` to reconfigure.` : "Check your credential configuration.";
21715
+ console.error(
21716
+ `Error: Tenant identity mismatch for ${profile ? `profile "${profile}"` : "configured credentials"}.
21717
+ Expected user: ${email2}
21718
+ ` + (identityResult.authenticatedEmail ? `Authenticated as: ${identityResult.authenticatedEmail}
21719
+ ` : "") + `This may indicate a DNS or configuration issue. ${profileHint}`
21720
+ );
21721
+ process.exit(1);
21722
+ }
21723
+ const profileLabel = profile ? `profile: ${profile}` : "env-var mode";
21724
+ console.error(
21725
+ `epimethian-mcp: connected to ${url} as ${email2} (${profileLabel})`
21726
+ );
21727
+ }
21321
21728
  var PageSchema = external_exports.object({
21322
21729
  id: external_exports.string(),
21323
21730
  title: external_exports.string(),
@@ -21366,12 +21773,22 @@ var UploadResultSchema = external_exports.object({
21366
21773
  })
21367
21774
  ).default([])
21368
21775
  });
21776
+ function sanitizeError(message) {
21777
+ let safe = message.slice(0, 500);
21778
+ safe = safe.replace(/Basic [A-Za-z0-9+/=]{20,}/g, "Basic [REDACTED]");
21779
+ safe = safe.replace(/Authorization:\s*\S+/gi, "Authorization: [REDACTED]");
21780
+ safe = safe.replace(/Bearer [A-Za-z0-9._-]{20,}/g, "Bearer [REDACTED]");
21781
+ return safe;
21782
+ }
21369
21783
  async function confluenceRequest(url, options = {}) {
21370
21784
  const cfg = await getConfig();
21371
21785
  const res = await fetch(url, { headers: cfg.jsonHeaders, ...options });
21372
21786
  if (!res.ok) {
21373
21787
  const body = await res.text();
21374
- throw new Error(`Confluence API error (${res.status}): ${body}`);
21788
+ console.error(`Confluence API error (${res.status}): ${body}`);
21789
+ throw new Error(
21790
+ `Confluence API error (${res.status}): ${sanitizeError(body)}`
21791
+ );
21375
21792
  }
21376
21793
  return res;
21377
21794
  }
@@ -21530,7 +21947,10 @@ async function uploadAttachment(pageId, fileData, filename, comment) {
21530
21947
  });
21531
21948
  if (!res.ok) {
21532
21949
  const body = await res.text();
21533
- throw new Error(`Confluence API error (${res.status}): ${body}`);
21950
+ console.error(`Confluence API error (${res.status}): ${body}`);
21951
+ throw new Error(
21952
+ `Confluence API error (${res.status}): ${sanitizeError(body)}`
21953
+ );
21534
21954
  }
21535
21955
  const data = UploadResultSchema.parse(await res.json());
21536
21956
  const att = data.results[0];
@@ -21592,360 +22012,374 @@ function toolResult(text) {
21592
22012
  return { content: [{ type: "text", text }] };
21593
22013
  }
21594
22014
  function toolError(err) {
21595
- const message = err instanceof Error ? err.message : String(err);
22015
+ const raw = err instanceof Error ? err.message : String(err);
22016
+ const message = sanitizeError(raw);
21596
22017
  return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
21597
22018
  }
21598
- var server = new McpServer({
21599
- name: "confluence",
21600
- version: "1.0.0"
21601
- });
21602
- server.registerTool(
21603
- "create_page",
21604
- {
21605
- description: "Create a new page in Confluence",
21606
- inputSchema: {
21607
- title: external_exports.string().describe("Page title"),
21608
- space_key: external_exports.string().describe("Confluence space key, e.g. 'DEV' or 'TEAM'"),
21609
- body: external_exports.string().describe(
21610
- "Page content \u2013 plain text or Confluence storage format (HTML)"
21611
- ),
21612
- parent_id: external_exports.string().optional().describe("Optional parent page ID")
22019
+ function tenantEcho(config2) {
22020
+ const host = new URL(config2.url).hostname;
22021
+ const mode = config2.profile ? `profile: ${config2.profile}` : "env-var mode";
22022
+ return `
22023
+ Tenant: ${host} (${mode})`;
22024
+ }
22025
+ function registerTools(server, config2) {
22026
+ const echo = tenantEcho(config2);
22027
+ server.registerTool(
22028
+ "create_page",
22029
+ {
22030
+ description: "Create a new page in Confluence",
22031
+ inputSchema: {
22032
+ title: external_exports.string().describe("Page title"),
22033
+ space_key: external_exports.string().describe("Confluence space key, e.g. 'DEV' or 'TEAM'"),
22034
+ body: external_exports.string().describe(
22035
+ "Page content \u2013 plain text or Confluence storage format (HTML)"
22036
+ ),
22037
+ parent_id: external_exports.string().optional().describe("Optional parent page ID")
22038
+ },
22039
+ annotations: { destructiveHint: false, idempotentHint: false }
21613
22040
  },
21614
- annotations: { destructiveHint: false, idempotentHint: false }
21615
- },
21616
- async ({ title, space_key, body, parent_id }) => {
21617
- try {
21618
- const spaceId = await resolveSpaceId(space_key);
21619
- const page = await createPage(spaceId, title, body, parent_id);
21620
- return toolResult(await formatPage(page, false));
21621
- } catch (err) {
21622
- return toolError(err);
21623
- }
21624
- }
21625
- );
21626
- server.registerTool(
21627
- "get_page",
21628
- {
21629
- description: "Read a Confluence page by ID",
21630
- inputSchema: {
21631
- page_id: external_exports.string().describe("The Confluence page ID"),
21632
- include_body: external_exports.boolean().default(true).describe("Whether to include the page body content")
22041
+ async ({ title, space_key, body, parent_id }) => {
22042
+ try {
22043
+ const spaceId = await resolveSpaceId(space_key);
22044
+ const page = await createPage(spaceId, title, body, parent_id);
22045
+ return toolResult(await formatPage(page, false) + echo);
22046
+ } catch (err) {
22047
+ return toolError(err);
22048
+ }
22049
+ }
22050
+ );
22051
+ server.registerTool(
22052
+ "get_page",
22053
+ {
22054
+ description: "Read a Confluence page by ID",
22055
+ inputSchema: {
22056
+ page_id: external_exports.string().describe("The Confluence page ID"),
22057
+ include_body: external_exports.boolean().default(true).describe("Whether to include the page body content")
22058
+ },
22059
+ annotations: { readOnlyHint: true }
21633
22060
  },
21634
- annotations: { readOnlyHint: true }
21635
- },
21636
- async ({ page_id, include_body }) => {
21637
- try {
21638
- const page = await getPage(page_id, include_body);
21639
- return toolResult(await formatPage(page, include_body));
21640
- } catch (err) {
21641
- return toolError(err);
21642
- }
21643
- }
21644
- );
21645
- server.registerTool(
21646
- "update_page",
21647
- {
21648
- description: "Update an existing Confluence page. Auto-increments version number.",
21649
- inputSchema: {
21650
- page_id: external_exports.string().describe("The Confluence page ID"),
21651
- title: external_exports.string().optional().describe("New title (omit to keep current)"),
21652
- body: external_exports.string().optional().describe("New body content in plain text or storage format"),
21653
- version_message: external_exports.string().optional().describe("Optional version comment")
22061
+ async ({ page_id, include_body }) => {
22062
+ try {
22063
+ const page = await getPage(page_id, include_body);
22064
+ return toolResult(await formatPage(page, include_body));
22065
+ } catch (err) {
22066
+ return toolError(err);
22067
+ }
22068
+ }
22069
+ );
22070
+ server.registerTool(
22071
+ "update_page",
22072
+ {
22073
+ description: "Update an existing Confluence page. Auto-increments version number.",
22074
+ inputSchema: {
22075
+ page_id: external_exports.string().describe("The Confluence page ID"),
22076
+ title: external_exports.string().optional().describe("New title (omit to keep current)"),
22077
+ body: external_exports.string().optional().describe("New body content in plain text or storage format"),
22078
+ version_message: external_exports.string().optional().describe("Optional version comment")
22079
+ },
22080
+ annotations: { destructiveHint: false, idempotentHint: true }
21654
22081
  },
21655
- annotations: { destructiveHint: false, idempotentHint: true }
21656
- },
21657
- async ({ page_id, title, body, version_message }) => {
21658
- try {
21659
- const { page, newVersion } = await updatePage(page_id, {
21660
- title,
21661
- body,
21662
- versionMessage: version_message
21663
- });
21664
- return toolResult(
21665
- `Updated: ${page.title} (ID: ${page.id}, version: ${newVersion})`
21666
- );
21667
- } catch (err) {
21668
- return toolError(err);
22082
+ async ({ page_id, title, body, version_message }) => {
22083
+ try {
22084
+ const { page, newVersion } = await updatePage(page_id, {
22085
+ title,
22086
+ body,
22087
+ versionMessage: version_message
22088
+ });
22089
+ return toolResult(
22090
+ `Updated: ${page.title} (ID: ${page.id}, version: ${newVersion})` + echo
22091
+ );
22092
+ } catch (err) {
22093
+ return toolError(err);
22094
+ }
21669
22095
  }
21670
- }
21671
- );
21672
- server.registerTool(
21673
- "delete_page",
21674
- {
21675
- description: "Delete a Confluence page by ID",
21676
- inputSchema: {
21677
- page_id: external_exports.string().describe("The Confluence page ID to delete")
22096
+ );
22097
+ server.registerTool(
22098
+ "delete_page",
22099
+ {
22100
+ description: "Delete a Confluence page by ID",
22101
+ inputSchema: {
22102
+ page_id: external_exports.string().describe("The Confluence page ID to delete")
22103
+ },
22104
+ annotations: { destructiveHint: true, idempotentHint: true }
21678
22105
  },
21679
- annotations: { destructiveHint: true, idempotentHint: true }
21680
- },
21681
- async ({ page_id }) => {
21682
- try {
21683
- await deletePage(page_id);
21684
- return toolResult(`Deleted page ${page_id}`);
21685
- } catch (err) {
21686
- return toolError(err);
21687
- }
21688
- }
21689
- );
21690
- server.registerTool(
21691
- "search_pages",
21692
- {
21693
- description: "Search Confluence pages using CQL (Confluence Query Language)",
21694
- inputSchema: {
21695
- cql: external_exports.string().describe(
21696
- `CQL query string (e.g., 'space = "DEV" AND title ~ "architecture"')`
21697
- ),
21698
- limit: external_exports.number().default(25).describe("Maximum results to return (default: 25)")
22106
+ async ({ page_id }) => {
22107
+ try {
22108
+ await deletePage(page_id);
22109
+ return toolResult(`Deleted page ${page_id}` + echo);
22110
+ } catch (err) {
22111
+ return toolError(err);
22112
+ }
22113
+ }
22114
+ );
22115
+ server.registerTool(
22116
+ "search_pages",
22117
+ {
22118
+ description: "Search Confluence pages using CQL (Confluence Query Language)",
22119
+ inputSchema: {
22120
+ cql: external_exports.string().describe(
22121
+ `CQL query string (e.g., 'space = "DEV" AND title ~ "architecture"')`
22122
+ ),
22123
+ limit: external_exports.number().default(25).describe("Maximum results to return (default: 25)")
22124
+ },
22125
+ annotations: { readOnlyHint: true }
21699
22126
  },
21700
- annotations: { readOnlyHint: true }
21701
- },
21702
- async ({ cql, limit }) => {
21703
- try {
21704
- const results = await searchPages(cql, limit);
21705
- if (results.length === 0) {
21706
- return toolResult("No pages found matching the query.");
21707
- }
21708
- const lines = [`Found ${results.length} page(s):`, ""];
21709
- for (const p of results) {
21710
- const spaceKey = p.spaceId ?? p.space?.key ?? "N/A";
21711
- lines.push(`- ${p.title} (ID: ${p.id}, space: ${spaceKey})`);
21712
- }
21713
- return toolResult(lines.join("\n"));
21714
- } catch (err) {
21715
- return toolError(err);
21716
- }
21717
- }
21718
- );
21719
- server.registerTool(
21720
- "list_pages",
21721
- {
21722
- description: "List pages in a Confluence space",
21723
- inputSchema: {
21724
- space_key: external_exports.string().describe("Confluence space key (e.g., 'DEV')"),
21725
- limit: external_exports.number().default(25).describe("Maximum results (default: 25)"),
21726
- status: external_exports.string().default("current").describe("Page status filter (default: 'current')")
22127
+ async ({ cql, limit }) => {
22128
+ try {
22129
+ const results = await searchPages(cql, limit);
22130
+ if (results.length === 0) {
22131
+ return toolResult("No pages found matching the query.");
22132
+ }
22133
+ const lines = [`Found ${results.length} page(s):`, ""];
22134
+ for (const p of results) {
22135
+ const spaceKey = p.spaceId ?? p.space?.key ?? "N/A";
22136
+ lines.push(`- ${p.title} (ID: ${p.id}, space: ${spaceKey})`);
22137
+ }
22138
+ return toolResult(lines.join("\n"));
22139
+ } catch (err) {
22140
+ return toolError(err);
22141
+ }
22142
+ }
22143
+ );
22144
+ server.registerTool(
22145
+ "list_pages",
22146
+ {
22147
+ description: "List pages in a Confluence space",
22148
+ inputSchema: {
22149
+ space_key: external_exports.string().describe("Confluence space key (e.g., 'DEV')"),
22150
+ limit: external_exports.number().default(25).describe("Maximum results (default: 25)"),
22151
+ status: external_exports.string().default("current").describe("Page status filter (default: 'current')")
22152
+ },
22153
+ annotations: { readOnlyHint: true }
21727
22154
  },
21728
- annotations: { readOnlyHint: true }
21729
- },
21730
- async ({ space_key, limit, status }) => {
21731
- try {
21732
- const spaceId = await resolveSpaceId(space_key);
21733
- const pages = await listPages(spaceId, limit, status);
21734
- if (pages.length === 0) {
21735
- return toolResult(`No pages found in space ${space_key}.`);
21736
- }
21737
- const lines = [`Pages in ${space_key} (${pages.length}):`, ""];
21738
- for (const p of pages) {
21739
- lines.push(`- ${p.title} (ID: ${p.id})`);
21740
- }
21741
- return toolResult(lines.join("\n"));
21742
- } catch (err) {
21743
- return toolError(err);
21744
- }
21745
- }
21746
- );
21747
- server.registerTool(
21748
- "get_page_children",
21749
- {
21750
- description: "Get child pages of a given Confluence page",
21751
- inputSchema: {
21752
- page_id: external_exports.string().describe("Parent page ID"),
21753
- limit: external_exports.number().default(25).describe("Maximum results (default: 25)")
22155
+ async ({ space_key, limit, status }) => {
22156
+ try {
22157
+ const spaceId = await resolveSpaceId(space_key);
22158
+ const pages = await listPages(spaceId, limit, status);
22159
+ if (pages.length === 0) {
22160
+ return toolResult(`No pages found in space ${space_key}.`);
22161
+ }
22162
+ const lines = [`Pages in ${space_key} (${pages.length}):`, ""];
22163
+ for (const p of pages) {
22164
+ lines.push(`- ${p.title} (ID: ${p.id})`);
22165
+ }
22166
+ return toolResult(lines.join("\n"));
22167
+ } catch (err) {
22168
+ return toolError(err);
22169
+ }
22170
+ }
22171
+ );
22172
+ server.registerTool(
22173
+ "get_page_children",
22174
+ {
22175
+ description: "Get child pages of a given Confluence page",
22176
+ inputSchema: {
22177
+ page_id: external_exports.string().describe("Parent page ID"),
22178
+ limit: external_exports.number().default(25).describe("Maximum results (default: 25)")
22179
+ },
22180
+ annotations: { readOnlyHint: true }
21754
22181
  },
21755
- annotations: { readOnlyHint: true }
21756
- },
21757
- async ({ page_id, limit }) => {
21758
- try {
21759
- const children = await getPageChildren(page_id, limit);
21760
- if (children.length === 0) {
21761
- return toolResult(`No child pages found for page ${page_id}.`);
21762
- }
21763
- const lines = [`Child pages (${children.length}):`, ""];
21764
- for (const p of children) {
21765
- lines.push(`- ${p.title} (ID: ${p.id})`);
21766
- }
21767
- return toolResult(lines.join("\n"));
21768
- } catch (err) {
21769
- return toolError(err);
21770
- }
21771
- }
21772
- );
21773
- server.registerTool(
21774
- "get_spaces",
21775
- {
21776
- description: "List available Confluence spaces",
21777
- inputSchema: {
21778
- limit: external_exports.number().default(25).describe("Maximum results (default: 25)"),
21779
- type: external_exports.string().optional().describe("Filter by space type (e.g., 'global', 'personal')")
22182
+ async ({ page_id, limit }) => {
22183
+ try {
22184
+ const children = await getPageChildren(page_id, limit);
22185
+ if (children.length === 0) {
22186
+ return toolResult(`No child pages found for page ${page_id}.`);
22187
+ }
22188
+ const lines = [`Child pages (${children.length}):`, ""];
22189
+ for (const p of children) {
22190
+ lines.push(`- ${p.title} (ID: ${p.id})`);
22191
+ }
22192
+ return toolResult(lines.join("\n"));
22193
+ } catch (err) {
22194
+ return toolError(err);
22195
+ }
22196
+ }
22197
+ );
22198
+ server.registerTool(
22199
+ "get_spaces",
22200
+ {
22201
+ description: "List available Confluence spaces",
22202
+ inputSchema: {
22203
+ limit: external_exports.number().default(25).describe("Maximum results (default: 25)"),
22204
+ type: external_exports.string().optional().describe("Filter by space type (e.g., 'global', 'personal')")
22205
+ },
22206
+ annotations: { readOnlyHint: true }
21780
22207
  },
21781
- annotations: { readOnlyHint: true }
21782
- },
21783
- async ({ limit, type }) => {
21784
- try {
21785
- const spaces = await getSpaces(limit, type);
21786
- if (spaces.length === 0) {
21787
- return toolResult("No spaces found.");
21788
- }
21789
- const lines = [`Found ${spaces.length} space(s):`, ""];
21790
- for (const s of spaces) {
21791
- lines.push(`- ${s.name} (key: ${s.key}, type: ${s.type})`);
21792
- }
21793
- return toolResult(lines.join("\n"));
21794
- } catch (err) {
21795
- return toolError(err);
21796
- }
21797
- }
21798
- );
21799
- server.registerTool(
21800
- "get_page_by_title",
21801
- {
21802
- description: "Look up a Confluence page by its title within a space",
21803
- inputSchema: {
21804
- title: external_exports.string().describe("Page title to search for"),
21805
- space_key: external_exports.string().describe("Confluence space key (e.g., 'DEV')"),
21806
- include_body: external_exports.boolean().default(false).describe("Whether to include the page body content")
22208
+ async ({ limit, type }) => {
22209
+ try {
22210
+ const spaces = await getSpaces(limit, type);
22211
+ if (spaces.length === 0) {
22212
+ return toolResult("No spaces found.");
22213
+ }
22214
+ const lines = [`Found ${spaces.length} space(s):`, ""];
22215
+ for (const s of spaces) {
22216
+ lines.push(`- ${s.name} (key: ${s.key}, type: ${s.type})`);
22217
+ }
22218
+ return toolResult(lines.join("\n"));
22219
+ } catch (err) {
22220
+ return toolError(err);
22221
+ }
22222
+ }
22223
+ );
22224
+ server.registerTool(
22225
+ "get_page_by_title",
22226
+ {
22227
+ description: "Look up a Confluence page by its title within a space",
22228
+ inputSchema: {
22229
+ title: external_exports.string().describe("Page title to search for"),
22230
+ space_key: external_exports.string().describe("Confluence space key (e.g., 'DEV')"),
22231
+ include_body: external_exports.boolean().default(false).describe("Whether to include the page body content")
22232
+ },
22233
+ annotations: { readOnlyHint: true }
21807
22234
  },
21808
- annotations: { readOnlyHint: true }
21809
- },
21810
- async ({ title, space_key, include_body }) => {
21811
- try {
21812
- const spaceId = await resolveSpaceId(space_key);
21813
- const page = await getPageByTitle(spaceId, title, include_body);
21814
- if (!page) {
22235
+ async ({ title, space_key, include_body }) => {
22236
+ try {
22237
+ const spaceId = await resolveSpaceId(space_key);
22238
+ const page = await getPageByTitle(spaceId, title, include_body);
22239
+ if (!page) {
22240
+ return toolResult(
22241
+ `No page found with title "${title}" in space ${space_key}.`
22242
+ );
22243
+ }
22244
+ return toolResult(await formatPage(page, include_body));
22245
+ } catch (err) {
22246
+ return toolError(err);
22247
+ }
22248
+ }
22249
+ );
22250
+ server.registerTool(
22251
+ "add_attachment",
22252
+ {
22253
+ description: "Upload a file as an attachment to a Confluence page. The file_path must be an absolute path under the current working directory.",
22254
+ inputSchema: {
22255
+ page_id: external_exports.string().describe("The Confluence page ID to attach the file to"),
22256
+ file_path: external_exports.string().describe("Absolute path to the file on the local filesystem"),
22257
+ filename: external_exports.string().optional().describe(
22258
+ "Filename to use in Confluence (defaults to the basename of file_path)"
22259
+ ),
22260
+ comment: external_exports.string().optional().describe("Optional comment for the attachment")
22261
+ },
22262
+ annotations: { destructiveHint: false, idempotentHint: false }
22263
+ },
22264
+ async ({ page_id, file_path, filename, comment }) => {
22265
+ try {
22266
+ const resolved = await (0, import_promises.realpath)((0, import_node_path.resolve)(file_path));
22267
+ const cwd = await (0, import_promises.realpath)(process.cwd());
22268
+ if (!resolved.startsWith(cwd + "/") && resolved !== cwd) {
22269
+ return toolError(
22270
+ new Error(
22271
+ `File path must be under the working directory (${cwd}). Got: ${resolved}`
22272
+ )
22273
+ );
22274
+ }
22275
+ const fileData = await (0, import_promises.readFile)(resolved);
22276
+ const name = filename ?? resolved.split("/").pop() ?? "attachment";
22277
+ const att = await uploadAttachment(page_id, fileData, name, comment);
21815
22278
  return toolResult(
21816
- `No page found with title "${title}" in space ${space_key}.`
22279
+ `Attached: ${att.title} (ID: ${att.id}, size: ${att.fileSize ?? "unknown"} bytes) to page ${page_id}` + echo
21817
22280
  );
21818
- }
21819
- return toolResult(await formatPage(page, include_body));
21820
- } catch (err) {
21821
- return toolError(err);
21822
- }
21823
- }
21824
- );
21825
- server.registerTool(
21826
- "add_attachment",
21827
- {
21828
- description: "Upload a file as an attachment to a Confluence page. The file_path must be an absolute path under the current working directory.",
21829
- inputSchema: {
21830
- page_id: external_exports.string().describe("The Confluence page ID to attach the file to"),
21831
- file_path: external_exports.string().describe("Absolute path to the file on the local filesystem"),
21832
- filename: external_exports.string().optional().describe(
21833
- "Filename to use in Confluence (defaults to the basename of file_path)"
21834
- ),
21835
- comment: external_exports.string().optional().describe("Optional comment for the attachment")
22281
+ } catch (err) {
22282
+ return toolError(err);
22283
+ }
22284
+ }
22285
+ );
22286
+ server.registerTool(
22287
+ "add_drawio_diagram",
22288
+ {
22289
+ description: "Add a draw.io diagram to a Confluence page. Uploads the diagram as an attachment and embeds it using the draw.io macro. Requires the draw.io app on the Confluence instance.",
22290
+ inputSchema: {
22291
+ page_id: external_exports.string().describe("The Confluence page ID to add the diagram to"),
22292
+ diagram_xml: external_exports.string().describe(
22293
+ "The draw.io diagram content in mxGraph XML format (the full XML starting with <mxfile>)"
22294
+ ),
22295
+ diagram_name: external_exports.string().regex(
22296
+ /^[a-zA-Z0-9_\-. ]+$/,
22297
+ "Diagram name may only contain letters, numbers, spaces, hyphens, underscores, and dots"
22298
+ ).describe(
22299
+ "Name for the diagram file (e.g., 'architecture.drawio'). Will have .drawio appended if not present."
22300
+ ),
22301
+ append: external_exports.boolean().default(true).describe(
22302
+ "If true, appends the diagram to existing page content. If false, replaces the page body."
22303
+ )
22304
+ },
22305
+ annotations: { destructiveHint: false, idempotentHint: false }
21836
22306
  },
21837
- annotations: { destructiveHint: false, idempotentHint: false }
21838
- },
21839
- async ({ page_id, file_path, filename, comment }) => {
21840
- try {
21841
- const resolved = await (0, import_promises.realpath)((0, import_node_path.resolve)(file_path));
21842
- const cwd = await (0, import_promises.realpath)(process.cwd());
21843
- if (!resolved.startsWith(cwd + "/") && resolved !== cwd) {
21844
- return toolError(
21845
- new Error(
21846
- `File path must be under the working directory (${cwd}). Got: ${resolved}`
21847
- )
22307
+ async ({ page_id, diagram_xml, diagram_name, append }) => {
22308
+ try {
22309
+ const filename = diagram_name.endsWith(".drawio") ? diagram_name : `${diagram_name}.drawio`;
22310
+ const tmpDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "drawio-"));
22311
+ try {
22312
+ const tmpPath = (0, import_node_path.join)(tmpDir, filename);
22313
+ await (0, import_promises.writeFile)(tmpPath, diagram_xml, "utf-8");
22314
+ const fileData = await (0, import_promises.readFile)(tmpPath);
22315
+ await uploadAttachment(page_id, fileData, filename);
22316
+ } finally {
22317
+ await (0, import_promises.rm)(tmpDir, { recursive: true, force: true });
22318
+ }
22319
+ const macro = [
22320
+ `<ac:structured-macro ac:name="drawio" ac:schema-version="1">`,
22321
+ ` <ac:parameter ac:name="diagramName">${escapeXml(filename)}</ac:parameter>`,
22322
+ ` <ac:parameter ac:name="attachment">${escapeXml(filename)}</ac:parameter>`,
22323
+ `</ac:structured-macro>`
22324
+ ].join("\n");
22325
+ const current = await getPage(page_id, true);
22326
+ const newVersion = (current.version?.number ?? 0) + 1;
22327
+ const existingBody = current.body?.storage?.value ?? current.body?.value ?? "";
22328
+ const newBody = append ? `${existingBody}
22329
+ ${macro}` : macro;
22330
+ const { page } = await updatePage(page_id, {
22331
+ body: newBody,
22332
+ versionMessage: `Added diagram: ${filename}`
22333
+ });
22334
+ return toolResult(
22335
+ `Diagram "${filename}" added to page ${page.title} (ID: ${page.id}, version: ${newVersion})` + echo
21848
22336
  );
22337
+ } catch (err) {
22338
+ return toolError(err);
21849
22339
  }
21850
- const fileData = await (0, import_promises.readFile)(resolved);
21851
- const name = filename ?? resolved.split("/").pop() ?? "attachment";
21852
- const att = await uploadAttachment(page_id, fileData, name, comment);
21853
- return toolResult(
21854
- `Attached: ${att.title} (ID: ${att.id}, size: ${att.fileSize ?? "unknown"} bytes) to page ${page_id}`
21855
- );
21856
- } catch (err) {
21857
- return toolError(err);
21858
- }
21859
- }
21860
- );
21861
- server.registerTool(
21862
- "add_drawio_diagram",
21863
- {
21864
- description: "Add a draw.io diagram to a Confluence page. Uploads the diagram as an attachment and embeds it using the draw.io macro. Requires the draw.io app on the Confluence instance.",
21865
- inputSchema: {
21866
- page_id: external_exports.string().describe("The Confluence page ID to add the diagram to"),
21867
- diagram_xml: external_exports.string().describe(
21868
- "The draw.io diagram content in mxGraph XML format (the full XML starting with <mxfile>)"
21869
- ),
21870
- diagram_name: external_exports.string().regex(
21871
- /^[a-zA-Z0-9_\-. ]+$/,
21872
- "Diagram name may only contain letters, numbers, spaces, hyphens, underscores, and dots"
21873
- ).describe(
21874
- "Name for the diagram file (e.g., 'architecture.drawio'). Will have .drawio appended if not present."
21875
- ),
21876
- append: external_exports.boolean().default(true).describe(
21877
- "If true, appends the diagram to existing page content. If false, replaces the page body."
21878
- )
22340
+ }
22341
+ );
22342
+ server.registerTool(
22343
+ "get_attachments",
22344
+ {
22345
+ description: "List attachments on a Confluence page",
22346
+ inputSchema: {
22347
+ page_id: external_exports.string().describe("The Confluence page ID"),
22348
+ limit: external_exports.number().default(25).describe("Maximum results (default: 25)")
22349
+ },
22350
+ annotations: { readOnlyHint: true }
21879
22351
  },
21880
- annotations: { destructiveHint: false, idempotentHint: false }
21881
- },
21882
- async ({ page_id, diagram_xml, diagram_name, append }) => {
21883
- try {
21884
- const filename = diagram_name.endsWith(".drawio") ? diagram_name : `${diagram_name}.drawio`;
21885
- const tmpDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "drawio-"));
22352
+ async ({ page_id, limit }) => {
21886
22353
  try {
21887
- const tmpPath = (0, import_node_path.join)(tmpDir, filename);
21888
- await (0, import_promises.writeFile)(tmpPath, diagram_xml, "utf-8");
21889
- const fileData = await (0, import_promises.readFile)(tmpPath);
21890
- await uploadAttachment(page_id, fileData, filename);
21891
- } finally {
21892
- await (0, import_promises.rm)(tmpDir, { recursive: true, force: true });
21893
- }
21894
- const macro = [
21895
- `<ac:structured-macro ac:name="drawio" ac:schema-version="1">`,
21896
- ` <ac:parameter ac:name="diagramName">${escapeXml(filename)}</ac:parameter>`,
21897
- ` <ac:parameter ac:name="attachment">${escapeXml(filename)}</ac:parameter>`,
21898
- `</ac:structured-macro>`
21899
- ].join("\n");
21900
- const current = await getPage(page_id, true);
21901
- const newVersion = (current.version?.number ?? 0) + 1;
21902
- const existingBody = current.body?.storage?.value ?? current.body?.value ?? "";
21903
- const newBody = append ? `${existingBody}
21904
- ${macro}` : macro;
21905
- const { page } = await updatePage(page_id, {
21906
- body: newBody,
21907
- versionMessage: `Added diagram: ${filename}`
21908
- });
21909
- return toolResult(
21910
- `Diagram "${filename}" added to page ${page.title} (ID: ${page.id}, version: ${newVersion})`
21911
- );
21912
- } catch (err) {
21913
- return toolError(err);
21914
- }
21915
- }
21916
- );
21917
- server.registerTool(
21918
- "get_attachments",
21919
- {
21920
- description: "List attachments on a Confluence page",
21921
- inputSchema: {
21922
- page_id: external_exports.string().describe("The Confluence page ID"),
21923
- limit: external_exports.number().default(25).describe("Maximum results (default: 25)")
21924
- },
21925
- annotations: { readOnlyHint: true }
21926
- },
21927
- async ({ page_id, limit }) => {
21928
- try {
21929
- const attachments = await getAttachments(page_id, limit);
21930
- if (attachments.length === 0) {
21931
- return toolResult(`No attachments found on page ${page_id}.`);
21932
- }
21933
- const lines = [
21934
- `Attachments on page ${page_id} (${attachments.length}):`,
21935
- ""
21936
- ];
21937
- for (const a of attachments) {
21938
- const size = a.extensions?.fileSize ? `${Math.round(a.extensions.fileSize / 1024)}KB` : "unknown size";
21939
- const mediaType = a.extensions?.mediaType ?? "unknown type";
21940
- lines.push(`- ${a.title} (ID: ${a.id}, ${mediaType}, ${size})`);
22354
+ const attachments = await getAttachments(page_id, limit);
22355
+ if (attachments.length === 0) {
22356
+ return toolResult(`No attachments found on page ${page_id}.`);
22357
+ }
22358
+ const lines = [
22359
+ `Attachments on page ${page_id} (${attachments.length}):`,
22360
+ ""
22361
+ ];
22362
+ for (const a of attachments) {
22363
+ const size = a.extensions?.fileSize ? `${Math.round(a.extensions.fileSize / 1024)}KB` : "unknown size";
22364
+ const mediaType = a.extensions?.mediaType ?? "unknown type";
22365
+ lines.push(`- ${a.title} (ID: ${a.id}, ${mediaType}, ${size})`);
22366
+ }
22367
+ return toolResult(lines.join("\n"));
22368
+ } catch (err) {
22369
+ return toolError(err);
21941
22370
  }
21942
- return toolResult(lines.join("\n"));
21943
- } catch (err) {
21944
- return toolError(err);
21945
22371
  }
21946
- }
21947
- );
22372
+ );
22373
+ }
21948
22374
  async function main() {
22375
+ const config2 = await getConfig();
22376
+ await validateStartup(config2);
22377
+ const serverName = config2.profile ? `confluence-${config2.profile}` : "confluence";
22378
+ const server = new McpServer({
22379
+ name: serverName,
22380
+ version: "1.0.0"
22381
+ });
22382
+ registerTools(server, config2);
21949
22383
  const transport = new StdioServerTransport();
21950
22384
  await server.connect(transport);
21951
22385
  }
@@ -21954,8 +22388,16 @@ async function main() {
21954
22388
  async function run() {
21955
22389
  const command = process.argv[2];
21956
22390
  if (command === "setup") {
22391
+ const idx = process.argv.indexOf("--profile");
22392
+ const profile = idx > -1 ? process.argv[idx + 1] : void 0;
21957
22393
  const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
21958
- await runSetup2();
22394
+ await runSetup2(profile);
22395
+ } else if (command === "profiles") {
22396
+ const { runProfiles: runProfiles2 } = await Promise.resolve().then(() => (init_profiles2(), profiles_exports));
22397
+ await runProfiles2();
22398
+ } else if (command === "status") {
22399
+ const { runStatus: runStatus2 } = await Promise.resolve().then(() => (init_status(), status_exports));
22400
+ await runStatus2();
21959
22401
  } else {
21960
22402
  await main();
21961
22403
  }