@de-otio/epimethian-mcp 2.0.1 → 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
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
  "use strict";
4
3
  var __create = Object.create;
5
4
  var __defProp = Object.defineProperty;
@@ -6804,20 +6803,28 @@ var require_dist = __commonJS({
6804
6803
  });
6805
6804
 
6806
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
+ }
6807
6814
  function exec(cmd, args) {
6808
6815
  return new Promise((resolve2, reject) => {
6809
- (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) => {
6810
6817
  if (err) {
6811
6818
  reject(new Error(stderr || err.message));
6812
6819
  } else {
6813
- resolve2(stdout2);
6820
+ resolve2(stdout3);
6814
6821
  }
6815
6822
  });
6816
6823
  });
6817
6824
  }
6818
- async function writeMacOS(password) {
6825
+ async function writeMacOS(account, password) {
6819
6826
  try {
6820
- await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", ACCOUNT]);
6827
+ await exec("security", ["delete-generic-password", "-s", SERVICE, "-a", account]);
6821
6828
  } catch {
6822
6829
  }
6823
6830
  await exec("security", [
@@ -6825,23 +6832,26 @@ async function writeMacOS(password) {
6825
6832
  "-s",
6826
6833
  SERVICE,
6827
6834
  "-a",
6828
- ACCOUNT,
6835
+ account,
6829
6836
  "-w",
6830
6837
  password,
6831
6838
  "-U"
6832
6839
  ]);
6833
6840
  }
6834
- async function readMacOS() {
6841
+ async function readMacOS(account) {
6835
6842
  return (await exec("security", [
6836
6843
  "find-generic-password",
6837
6844
  "-s",
6838
6845
  SERVICE,
6839
6846
  "-a",
6840
- ACCOUNT,
6847
+ account,
6841
6848
  "-w"
6842
6849
  ])).trim();
6843
6850
  }
6844
- 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) {
6845
6855
  return new Promise((resolve2, reject) => {
6846
6856
  const proc = (0, import_node_child_process.spawn)("secret-tool", [
6847
6857
  "store",
@@ -6850,7 +6860,7 @@ async function writeLinux(password) {
6850
6860
  "service",
6851
6861
  SERVICE,
6852
6862
  "account",
6853
- ACCOUNT
6863
+ account
6854
6864
  ]);
6855
6865
  proc.stdin.write(password);
6856
6866
  proc.stdin.end();
@@ -6861,55 +6871,122 @@ async function writeLinux(password) {
6861
6871
  proc.on("error", reject);
6862
6872
  });
6863
6873
  }
6864
- async function readLinux() {
6874
+ async function readLinux(account) {
6865
6875
  return (await exec("secret-tool", [
6866
6876
  "lookup",
6867
6877
  "service",
6868
6878
  SERVICE,
6869
6879
  "account",
6870
- ACCOUNT
6880
+ account
6871
6881
  ])).trim();
6872
6882
  }
6873
- 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);
6874
6900
  const json = JSON.stringify(creds);
6875
6901
  if (process.platform === "darwin") {
6876
- await writeMacOS(json);
6902
+ await writeMacOS(account, json);
6877
6903
  } else if (process.platform === "linux") {
6878
- await writeLinux(json);
6904
+ await writeLinux(account, json);
6879
6905
  } else {
6880
6906
  throw new Error(`Keychain not supported on ${process.platform}`);
6881
6907
  }
6882
6908
  }
6883
- async function readFromKeychain() {
6909
+ async function readFromKeychain(profile) {
6910
+ const account = resolveAccount(profile);
6911
+ let raw;
6884
6912
  try {
6885
- let raw;
6886
6913
  if (process.platform === "darwin") {
6887
- raw = await readMacOS();
6914
+ raw = await readMacOS(account);
6888
6915
  } else if (process.platform === "linux") {
6889
- raw = await readLinux();
6916
+ raw = await readLinux(account);
6890
6917
  } else {
6891
6918
  return null;
6892
6919
  }
6893
- const parsed = JSON.parse(raw);
6894
- if (parsed && typeof parsed.apiToken === "string") {
6895
- return parsed;
6896
- }
6897
- return null;
6898
6920
  } catch {
6899
6921
  return null;
6900
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
+ }
6901
6948
  }
6902
- var import_node_child_process, SERVICE, ACCOUNT;
6949
+ var import_node_child_process, SERVICE, LEGACY_ACCOUNT, PROFILE_NAME_RE;
6903
6950
  var init_keychain = __esm({
6904
6951
  "src/shared/keychain.ts"() {
6905
6952
  "use strict";
6906
6953
  import_node_child_process = require("node:child_process");
6907
6954
  SERVICE = "epimethian-mcp";
6908
- ACCOUNT = "confluence-credentials";
6955
+ LEGACY_ACCOUNT = "confluence-credentials";
6956
+ PROFILE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
6909
6957
  }
6910
6958
  });
6911
6959
 
6912
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
+ }
6913
6990
  async function testConnection(url, email2, apiToken) {
6914
6991
  const endpoint = `${url.replace(/\/+$/, "")}/wiki/api/v2/spaces?limit=1`;
6915
6992
  const auth = Buffer.from(`${email2}:${apiToken}`).toString("base64");
@@ -6947,6 +7024,76 @@ var init_test_connection = __esm({
6947
7024
  }
6948
7025
  });
6949
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
+
6950
7097
  // src/cli/setup.ts
6951
7098
  var setup_exports = {};
6952
7099
  __export(setup_exports, {
@@ -6982,15 +7129,22 @@ function readPassword(prompt) {
6982
7129
  import_node_process2.stdin.on("data", onData);
6983
7130
  });
6984
7131
  }
6985
- async function runSetup() {
7132
+ async function runSetup(profile) {
6986
7133
  if (!import_node_process2.stdin.isTTY) {
6987
7134
  console.error(
6988
7135
  "Error: setup requires an interactive terminal.\nFor non-interactive environments, set CONFLUENCE_URL, CONFLUENCE_EMAIL, and CONFLUENCE_API_TOKEN as environment variables."
6989
7136
  );
6990
7137
  process.exit(1);
6991
7138
  }
6992
- console.log("Epimethian MCP - Confluence credential setup\n");
6993
- 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);
6994
7148
  const rl = readline.createInterface({ input: import_node_process2.stdin, output: import_node_process2.stdout });
6995
7149
  try {
6996
7150
  const defaultUrl = existing?.url ?? "";
@@ -7006,6 +7160,21 @@ async function runSetup() {
7006
7160
  console.error("Error: URL must start with https://");
7007
7161
  process.exit(1);
7008
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
+ }
7009
7178
  const defaultEmail = existing?.email ?? "";
7010
7179
  const emailPrompt = defaultEmail ? `Email [${defaultEmail}]: ` : "Email: ";
7011
7180
  let email2 = (await rl.question(emailPrompt)).trim();
@@ -7030,8 +7199,17 @@ async function runSetup() {
7030
7199
  }
7031
7200
  console.log(`${result.message}
7032
7201
  `);
7033
- await saveToKeychain({ url, email: email2, apiToken });
7034
- 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
+ }
7035
7213
  console.log(`Available tools (${TOOLS.length}):`);
7036
7214
  console.log(` ${TOOLS.join(", ")}`);
7037
7215
  console.log(
@@ -7049,6 +7227,7 @@ var init_setup = __esm({
7049
7227
  import_node_process2 = require("node:process");
7050
7228
  init_test_connection();
7051
7229
  init_keychain();
7230
+ init_profiles();
7052
7231
  TOOLS = [
7053
7232
  "create_page",
7054
7233
  "get_page",
@@ -7066,6 +7245,164 @@ var init_setup = __esm({
7066
7245
  }
7067
7246
  });
7068
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
+
7069
7406
  // node_modules/zod/v3/external.js
7070
7407
  var external_exports = {};
7071
7408
  __export(external_exports, {
@@ -21286,29 +21623,64 @@ var import_node_path = require("node:path");
21286
21623
 
21287
21624
  // src/server/confluence-client.ts
21288
21625
  init_keychain();
21626
+ init_test_connection();
21289
21627
  var _config = null;
21290
- async function getConfig() {
21291
- if (_config) return _config;
21292
- let url = process.env.CONFLUENCE_URL?.replace(/\/$/, "") || "";
21293
- let email2 = process.env.CONFLUENCE_EMAIL || "";
21294
- let apiToken = process.env.CONFLUENCE_API_TOKEN || "";
21295
- if (!url || !email2 || !apiToken) {
21296
- const keychainCreds = await readFromKeychain();
21297
- if (keychainCreds) {
21298
- url = url || keychainCreds.url.replace(/\/$/, "");
21299
- email2 = email2 || keychainCreds.email;
21300
- apiToken = apiToken || keychainCreds.apiToken;
21301
- }
21302
- }
21303
- 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) {
21304
21664
  console.error(
21305
- "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.`
21306
21668
  );
21307
21669
  process.exit(1);
21308
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();
21309
21679
  const authHeader = "Basic " + Buffer.from(`${email2}:${apiToken}`).toString("base64");
21310
- _config = {
21680
+ _config = Object.freeze({
21311
21681
  url,
21682
+ email: email2,
21683
+ profile,
21312
21684
  apiV2: `${url}/wiki/api/v2`,
21313
21685
  apiV1: `${url}/wiki/rest/api`,
21314
21686
  authHeader,
@@ -21316,9 +21688,43 @@ async function getConfig() {
21316
21688
  Authorization: authHeader,
21317
21689
  "Content-Type": "application/json"
21318
21690
  }
21319
- };
21691
+ });
21320
21692
  return _config;
21321
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
+ }
21322
21728
  var PageSchema = external_exports.object({
21323
21729
  id: external_exports.string(),
21324
21730
  title: external_exports.string(),
@@ -21367,12 +21773,22 @@ var UploadResultSchema = external_exports.object({
21367
21773
  })
21368
21774
  ).default([])
21369
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
+ }
21370
21783
  async function confluenceRequest(url, options = {}) {
21371
21784
  const cfg = await getConfig();
21372
21785
  const res = await fetch(url, { headers: cfg.jsonHeaders, ...options });
21373
21786
  if (!res.ok) {
21374
21787
  const body = await res.text();
21375
- 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
+ );
21376
21792
  }
21377
21793
  return res;
21378
21794
  }
@@ -21531,7 +21947,10 @@ async function uploadAttachment(pageId, fileData, filename, comment) {
21531
21947
  });
21532
21948
  if (!res.ok) {
21533
21949
  const body = await res.text();
21534
- 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
+ );
21535
21954
  }
21536
21955
  const data = UploadResultSchema.parse(await res.json());
21537
21956
  const att = data.results[0];
@@ -21593,360 +22012,374 @@ function toolResult(text) {
21593
22012
  return { content: [{ type: "text", text }] };
21594
22013
  }
21595
22014
  function toolError(err) {
21596
- const message = err instanceof Error ? err.message : String(err);
22015
+ const raw = err instanceof Error ? err.message : String(err);
22016
+ const message = sanitizeError(raw);
21597
22017
  return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
21598
22018
  }
21599
- var server = new McpServer({
21600
- name: "confluence",
21601
- version: "1.0.0"
21602
- });
21603
- server.registerTool(
21604
- "create_page",
21605
- {
21606
- description: "Create a new page in Confluence",
21607
- inputSchema: {
21608
- title: external_exports.string().describe("Page title"),
21609
- space_key: external_exports.string().describe("Confluence space key, e.g. 'DEV' or 'TEAM'"),
21610
- body: external_exports.string().describe(
21611
- "Page content \u2013 plain text or Confluence storage format (HTML)"
21612
- ),
21613
- 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 }
21614
22040
  },
21615
- annotations: { destructiveHint: false, idempotentHint: false }
21616
- },
21617
- async ({ title, space_key, body, parent_id }) => {
21618
- try {
21619
- const spaceId = await resolveSpaceId(space_key);
21620
- const page = await createPage(spaceId, title, body, parent_id);
21621
- return toolResult(await formatPage(page, false));
21622
- } catch (err) {
21623
- return toolError(err);
21624
- }
21625
- }
21626
- );
21627
- server.registerTool(
21628
- "get_page",
21629
- {
21630
- description: "Read a Confluence page by ID",
21631
- inputSchema: {
21632
- page_id: external_exports.string().describe("The Confluence page ID"),
21633
- 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 }
21634
22060
  },
21635
- annotations: { readOnlyHint: true }
21636
- },
21637
- async ({ page_id, include_body }) => {
21638
- try {
21639
- const page = await getPage(page_id, include_body);
21640
- return toolResult(await formatPage(page, include_body));
21641
- } catch (err) {
21642
- return toolError(err);
21643
- }
21644
- }
21645
- );
21646
- server.registerTool(
21647
- "update_page",
21648
- {
21649
- description: "Update an existing Confluence page. Auto-increments version number.",
21650
- inputSchema: {
21651
- page_id: external_exports.string().describe("The Confluence page ID"),
21652
- title: external_exports.string().optional().describe("New title (omit to keep current)"),
21653
- body: external_exports.string().optional().describe("New body content in plain text or storage format"),
21654
- 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 }
21655
22081
  },
21656
- annotations: { destructiveHint: false, idempotentHint: true }
21657
- },
21658
- async ({ page_id, title, body, version_message }) => {
21659
- try {
21660
- const { page, newVersion } = await updatePage(page_id, {
21661
- title,
21662
- body,
21663
- versionMessage: version_message
21664
- });
21665
- return toolResult(
21666
- `Updated: ${page.title} (ID: ${page.id}, version: ${newVersion})`
21667
- );
21668
- } catch (err) {
21669
- 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
+ }
21670
22095
  }
21671
- }
21672
- );
21673
- server.registerTool(
21674
- "delete_page",
21675
- {
21676
- description: "Delete a Confluence page by ID",
21677
- inputSchema: {
21678
- 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 }
21679
22105
  },
21680
- annotations: { destructiveHint: true, idempotentHint: true }
21681
- },
21682
- async ({ page_id }) => {
21683
- try {
21684
- await deletePage(page_id);
21685
- return toolResult(`Deleted page ${page_id}`);
21686
- } catch (err) {
21687
- return toolError(err);
21688
- }
21689
- }
21690
- );
21691
- server.registerTool(
21692
- "search_pages",
21693
- {
21694
- description: "Search Confluence pages using CQL (Confluence Query Language)",
21695
- inputSchema: {
21696
- cql: external_exports.string().describe(
21697
- `CQL query string (e.g., 'space = "DEV" AND title ~ "architecture"')`
21698
- ),
21699
- 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 }
21700
22126
  },
21701
- annotations: { readOnlyHint: true }
21702
- },
21703
- async ({ cql, limit }) => {
21704
- try {
21705
- const results = await searchPages(cql, limit);
21706
- if (results.length === 0) {
21707
- return toolResult("No pages found matching the query.");
21708
- }
21709
- const lines = [`Found ${results.length} page(s):`, ""];
21710
- for (const p of results) {
21711
- const spaceKey = p.spaceId ?? p.space?.key ?? "N/A";
21712
- lines.push(`- ${p.title} (ID: ${p.id}, space: ${spaceKey})`);
21713
- }
21714
- return toolResult(lines.join("\n"));
21715
- } catch (err) {
21716
- return toolError(err);
21717
- }
21718
- }
21719
- );
21720
- server.registerTool(
21721
- "list_pages",
21722
- {
21723
- description: "List pages in a Confluence space",
21724
- inputSchema: {
21725
- space_key: external_exports.string().describe("Confluence space key (e.g., 'DEV')"),
21726
- limit: external_exports.number().default(25).describe("Maximum results (default: 25)"),
21727
- 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 }
21728
22154
  },
21729
- annotations: { readOnlyHint: true }
21730
- },
21731
- async ({ space_key, limit, status }) => {
21732
- try {
21733
- const spaceId = await resolveSpaceId(space_key);
21734
- const pages = await listPages(spaceId, limit, status);
21735
- if (pages.length === 0) {
21736
- return toolResult(`No pages found in space ${space_key}.`);
21737
- }
21738
- const lines = [`Pages in ${space_key} (${pages.length}):`, ""];
21739
- for (const p of pages) {
21740
- lines.push(`- ${p.title} (ID: ${p.id})`);
21741
- }
21742
- return toolResult(lines.join("\n"));
21743
- } catch (err) {
21744
- return toolError(err);
21745
- }
21746
- }
21747
- );
21748
- server.registerTool(
21749
- "get_page_children",
21750
- {
21751
- description: "Get child pages of a given Confluence page",
21752
- inputSchema: {
21753
- page_id: external_exports.string().describe("Parent page ID"),
21754
- 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 }
21755
22181
  },
21756
- annotations: { readOnlyHint: true }
21757
- },
21758
- async ({ page_id, limit }) => {
21759
- try {
21760
- const children = await getPageChildren(page_id, limit);
21761
- if (children.length === 0) {
21762
- return toolResult(`No child pages found for page ${page_id}.`);
21763
- }
21764
- const lines = [`Child pages (${children.length}):`, ""];
21765
- for (const p of children) {
21766
- lines.push(`- ${p.title} (ID: ${p.id})`);
21767
- }
21768
- return toolResult(lines.join("\n"));
21769
- } catch (err) {
21770
- return toolError(err);
21771
- }
21772
- }
21773
- );
21774
- server.registerTool(
21775
- "get_spaces",
21776
- {
21777
- description: "List available Confluence spaces",
21778
- inputSchema: {
21779
- limit: external_exports.number().default(25).describe("Maximum results (default: 25)"),
21780
- 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 }
21781
22207
  },
21782
- annotations: { readOnlyHint: true }
21783
- },
21784
- async ({ limit, type }) => {
21785
- try {
21786
- const spaces = await getSpaces(limit, type);
21787
- if (spaces.length === 0) {
21788
- return toolResult("No spaces found.");
21789
- }
21790
- const lines = [`Found ${spaces.length} space(s):`, ""];
21791
- for (const s of spaces) {
21792
- lines.push(`- ${s.name} (key: ${s.key}, type: ${s.type})`);
21793
- }
21794
- return toolResult(lines.join("\n"));
21795
- } catch (err) {
21796
- return toolError(err);
21797
- }
21798
- }
21799
- );
21800
- server.registerTool(
21801
- "get_page_by_title",
21802
- {
21803
- description: "Look up a Confluence page by its title within a space",
21804
- inputSchema: {
21805
- title: external_exports.string().describe("Page title to search for"),
21806
- space_key: external_exports.string().describe("Confluence space key (e.g., 'DEV')"),
21807
- 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 }
21808
22234
  },
21809
- annotations: { readOnlyHint: true }
21810
- },
21811
- async ({ title, space_key, include_body }) => {
21812
- try {
21813
- const spaceId = await resolveSpaceId(space_key);
21814
- const page = await getPageByTitle(spaceId, title, include_body);
21815
- 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);
21816
22278
  return toolResult(
21817
- `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
21818
22280
  );
21819
- }
21820
- return toolResult(await formatPage(page, include_body));
21821
- } catch (err) {
21822
- return toolError(err);
21823
- }
21824
- }
21825
- );
21826
- server.registerTool(
21827
- "add_attachment",
21828
- {
21829
- description: "Upload a file as an attachment to a Confluence page. The file_path must be an absolute path under the current working directory.",
21830
- inputSchema: {
21831
- page_id: external_exports.string().describe("The Confluence page ID to attach the file to"),
21832
- file_path: external_exports.string().describe("Absolute path to the file on the local filesystem"),
21833
- filename: external_exports.string().optional().describe(
21834
- "Filename to use in Confluence (defaults to the basename of file_path)"
21835
- ),
21836
- 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 }
21837
22306
  },
21838
- annotations: { destructiveHint: false, idempotentHint: false }
21839
- },
21840
- async ({ page_id, file_path, filename, comment }) => {
21841
- try {
21842
- const resolved = await (0, import_promises.realpath)((0, import_node_path.resolve)(file_path));
21843
- const cwd = await (0, import_promises.realpath)(process.cwd());
21844
- if (!resolved.startsWith(cwd + "/") && resolved !== cwd) {
21845
- return toolError(
21846
- new Error(
21847
- `File path must be under the working directory (${cwd}). Got: ${resolved}`
21848
- )
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
21849
22336
  );
22337
+ } catch (err) {
22338
+ return toolError(err);
21850
22339
  }
21851
- const fileData = await (0, import_promises.readFile)(resolved);
21852
- const name = filename ?? resolved.split("/").pop() ?? "attachment";
21853
- const att = await uploadAttachment(page_id, fileData, name, comment);
21854
- return toolResult(
21855
- `Attached: ${att.title} (ID: ${att.id}, size: ${att.fileSize ?? "unknown"} bytes) to page ${page_id}`
21856
- );
21857
- } catch (err) {
21858
- return toolError(err);
21859
- }
21860
- }
21861
- );
21862
- server.registerTool(
21863
- "add_drawio_diagram",
21864
- {
21865
- 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.",
21866
- inputSchema: {
21867
- page_id: external_exports.string().describe("The Confluence page ID to add the diagram to"),
21868
- diagram_xml: external_exports.string().describe(
21869
- "The draw.io diagram content in mxGraph XML format (the full XML starting with <mxfile>)"
21870
- ),
21871
- diagram_name: external_exports.string().regex(
21872
- /^[a-zA-Z0-9_\-. ]+$/,
21873
- "Diagram name may only contain letters, numbers, spaces, hyphens, underscores, and dots"
21874
- ).describe(
21875
- "Name for the diagram file (e.g., 'architecture.drawio'). Will have .drawio appended if not present."
21876
- ),
21877
- append: external_exports.boolean().default(true).describe(
21878
- "If true, appends the diagram to existing page content. If false, replaces the page body."
21879
- )
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 }
21880
22351
  },
21881
- annotations: { destructiveHint: false, idempotentHint: false }
21882
- },
21883
- async ({ page_id, diagram_xml, diagram_name, append }) => {
21884
- try {
21885
- const filename = diagram_name.endsWith(".drawio") ? diagram_name : `${diagram_name}.drawio`;
21886
- const tmpDir = await (0, import_promises.mkdtemp)((0, import_node_path.join)((0, import_node_os.tmpdir)(), "drawio-"));
22352
+ async ({ page_id, limit }) => {
21887
22353
  try {
21888
- const tmpPath = (0, import_node_path.join)(tmpDir, filename);
21889
- await (0, import_promises.writeFile)(tmpPath, diagram_xml, "utf-8");
21890
- const fileData = await (0, import_promises.readFile)(tmpPath);
21891
- await uploadAttachment(page_id, fileData, filename);
21892
- } finally {
21893
- await (0, import_promises.rm)(tmpDir, { recursive: true, force: true });
21894
- }
21895
- const macro = [
21896
- `<ac:structured-macro ac:name="drawio" ac:schema-version="1">`,
21897
- ` <ac:parameter ac:name="diagramName">${escapeXml(filename)}</ac:parameter>`,
21898
- ` <ac:parameter ac:name="attachment">${escapeXml(filename)}</ac:parameter>`,
21899
- `</ac:structured-macro>`
21900
- ].join("\n");
21901
- const current = await getPage(page_id, true);
21902
- const newVersion = (current.version?.number ?? 0) + 1;
21903
- const existingBody = current.body?.storage?.value ?? current.body?.value ?? "";
21904
- const newBody = append ? `${existingBody}
21905
- ${macro}` : macro;
21906
- const { page } = await updatePage(page_id, {
21907
- body: newBody,
21908
- versionMessage: `Added diagram: ${filename}`
21909
- });
21910
- return toolResult(
21911
- `Diagram "${filename}" added to page ${page.title} (ID: ${page.id}, version: ${newVersion})`
21912
- );
21913
- } catch (err) {
21914
- return toolError(err);
21915
- }
21916
- }
21917
- );
21918
- server.registerTool(
21919
- "get_attachments",
21920
- {
21921
- description: "List attachments on a Confluence page",
21922
- inputSchema: {
21923
- page_id: external_exports.string().describe("The Confluence page ID"),
21924
- limit: external_exports.number().default(25).describe("Maximum results (default: 25)")
21925
- },
21926
- annotations: { readOnlyHint: true }
21927
- },
21928
- async ({ page_id, limit }) => {
21929
- try {
21930
- const attachments = await getAttachments(page_id, limit);
21931
- if (attachments.length === 0) {
21932
- return toolResult(`No attachments found on page ${page_id}.`);
21933
- }
21934
- const lines = [
21935
- `Attachments on page ${page_id} (${attachments.length}):`,
21936
- ""
21937
- ];
21938
- for (const a of attachments) {
21939
- const size = a.extensions?.fileSize ? `${Math.round(a.extensions.fileSize / 1024)}KB` : "unknown size";
21940
- const mediaType = a.extensions?.mediaType ?? "unknown type";
21941
- 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);
21942
22370
  }
21943
- return toolResult(lines.join("\n"));
21944
- } catch (err) {
21945
- return toolError(err);
21946
22371
  }
21947
- }
21948
- );
22372
+ );
22373
+ }
21949
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);
21950
22383
  const transport = new StdioServerTransport();
21951
22384
  await server.connect(transport);
21952
22385
  }
@@ -21955,8 +22388,16 @@ async function main() {
21955
22388
  async function run() {
21956
22389
  const command = process.argv[2];
21957
22390
  if (command === "setup") {
22391
+ const idx = process.argv.indexOf("--profile");
22392
+ const profile = idx > -1 ? process.argv[idx + 1] : void 0;
21958
22393
  const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
21959
- 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();
21960
22401
  } else {
21961
22402
  await main();
21962
22403
  }