@de-otio/epimethian-mcp 3.0.0 → 4.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/README.md CHANGED
@@ -2,7 +2,21 @@
2
2
 
3
3
  Confluence Cloud tools for AI assistants via the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP). (not associated with or endorsed by Atlassian)
4
4
 
5
- > **Note:** For most Confluence use cases, the official [Atlassian Rovo MCP server](https://github.com/atlassian/mcp-server-atlassian) may be sufficient. Use Epimethian if you need draw.io diagram support, OS keychain credential storage, or attribution tracking on managed pages.
5
+ ## Why use this?
6
+
7
+ The official [Atlassian MCP server](https://github.com/atlassian/atlassian-mcp-server) covers basic Confluence and Jira access. Epimethian targets gaps that matter for consultants, power users, and teams with strict security requirements:
8
+
9
+ - **OS keychain credential storage** — API tokens are stored in macOS Keychain or Linux libsecret, never in plaintext config files. Setup uses masked input so tokens don't leak into terminal scrollback.
10
+ - **Multi-tenant profile isolation** — Each Atlassian tenant gets its own named profile with fully separate credentials and keychain entries. No risk of cross-tenant writes when switching between clients.
11
+ - **Tenant-aware write safety** — Write operations echo the target tenant so the AI agent (and you) always see where changes are going before they land.
12
+ - **draw.io diagram support** — Create and embed draw.io diagrams directly in Confluence pages, something the official server doesn't expose.
13
+ - **Attribution tracking** — Managed pages carry metadata so you can trace which AI-assisted edits touched which content.
14
+
15
+ If you don't need any of the above, the official Atlassian server is a fine choice.
16
+
17
+ ## How it works
18
+
19
+ Epimethian runs as a local MCP server that your AI agent (Claude Code, Cursor, etc.) talks to over stdio. On startup it reads a profile name from the environment, pulls the matching credentials from your OS keychain, validates the connection against Confluence Cloud, and then exposes a set of tools the agent can call. All Confluence API calls go directly from your machine to Atlassian — there is no intermediate service.
6
20
 
7
21
  ## Quick Start
8
22
 
@@ -10,14 +24,16 @@ Tell your AI agent:
10
24
 
11
25
  > Install and configure the Epimethian MCP server. See https://github.com/de-otio/epimethian-mcp
12
26
 
27
+ For a detailed agent-facing guide (installation, configuration, profile management, uninstallation), see [install-agent.md](install-agent.md) or run `epimethian-mcp agent-guide` after installation.
28
+
13
29
  Or install manually:
14
30
 
15
31
  ```bash
16
32
  npm install -g @de-otio/epimethian-mcp
17
- epimethian-mcp setup
33
+ epimethian-mcp setup --profile <name>
18
34
  ```
19
35
 
20
- The `setup` command prompts for your Confluence URL, email, and API token (masked input), tests the connection, and stores credentials securely in your OS keychain.
36
+ The `setup` command prompts for your Confluence URL, email, and API token (masked input), tests the connection, and stores all credentials securely in your OS keychain under the named profile.
21
37
 
22
38
  ## MCP Configuration
23
39
 
@@ -29,18 +45,39 @@ Add to your `.mcp.json` (or equivalent MCP client config):
29
45
  "confluence": {
30
46
  "command": "epimethian-mcp",
31
47
  "env": {
32
- "CONFLUENCE_URL": "https://yoursite.atlassian.net",
33
- "CONFLUENCE_EMAIL": "user@example.com"
48
+ "CONFLUENCE_PROFILE": "my-profile"
34
49
  }
35
50
  }
36
51
  }
37
52
  }
38
53
  ```
39
54
 
40
- The API token is read from the OS keychain at startup. **Do not put it in config files.**
55
+ All credentials (URL, email, token) are read from the OS keychain at startup. **Only the profile name goes in config files.**
41
56
 
42
57
  For IDE-hosted agents, use the absolute path from `which epimethian-mcp` as the `command` value.
43
58
 
59
+ ## Multi-Tenant Support
60
+
61
+ Consultants and developers working across multiple Atlassian tenants can create a profile per tenant:
62
+
63
+ ```bash
64
+ epimethian-mcp setup --profile jambit
65
+ epimethian-mcp setup --profile acme-corp
66
+ ```
67
+
68
+ Each project's `.mcp.json` specifies which profile to use. Profiles are fully isolated — separate keychain entries, separate Confluence instances, separate MCP server names (`confluence-jambit`, `confluence-acme-corp`).
69
+
70
+ Manage profiles:
71
+
72
+ ```bash
73
+ epimethian-mcp profiles # list all
74
+ epimethian-mcp profiles --verbose # show URLs and emails
75
+ CONFLUENCE_PROFILE=jambit epimethian-mcp status # test connection
76
+ epimethian-mcp profiles --remove <name> # delete profile and credentials
77
+ ```
78
+
79
+ The `--remove` command deletes the profile's keychain entry and registry record after interactive confirmation. For non-interactive environments (CI, agent shell sessions), pass `--force` to skip the prompt.
80
+
44
81
  ## Tools
45
82
 
46
83
  | Tool | Description |
@@ -60,10 +97,13 @@ For IDE-hosted agents, use the absolute path from `which epimethian-mcp` as the
60
97
 
61
98
  ## Credential Security
62
99
 
63
- - API tokens are stored in the OS keychain (macOS Keychain / Linux libsecret)
100
+ - Credentials are stored per-profile in the OS keychain (macOS Keychain / Linux libsecret)
101
+ - URL, email, and API token are stored as an atomic unit — no mixing across profiles
64
102
  - Tokens are never written to disk in plaintext
65
103
  - The `setup` command uses masked input so tokens don't appear in terminal scrollback
66
- - For CI/headless environments, set `CONFLUENCE_API_TOKEN` as an environment variable injected by your secret manager
104
+ - Startup validation verifies credentials and tenant identity before accepting tool calls
105
+ - Write operations include a tenant echo so the target is always visible
106
+ - For CI/headless environments, set all three env vars (`CONFLUENCE_URL`, `CONFLUENCE_EMAIL`, `CONFLUENCE_API_TOKEN`) — partial combinations are rejected
67
107
 
68
108
  ## Development
69
109
 
package/dist/cli/index.js CHANGED
@@ -7403,6 +7403,29 @@ var init_status = __esm({
7403
7403
  }
7404
7404
  });
7405
7405
 
7406
+ // install-agent.md
7407
+ var install_agent_default;
7408
+ var init_install_agent = __esm({
7409
+ "install-agent.md"() {
7410
+ install_agent_default = '# Epimethian MCP - Agent Installation Guide\n\n> This document is for AI agents installing and configuring the Epimethian MCP server on behalf of a user.\n\n## Verification\n\nThe official package is **`@de-otio/epimethian-mcp`** (scoped under `@de-otio`).\n\nDo NOT install unscoped alternatives like `epimethian-mcp` \u2014 these are unofficial and potentially unsafe.\n\n## Prerequisites\n\n- Node.js 18 or later\n- npm on PATH\n\n## Step 1: Install\n\n```bash\nnpm install -g @de-otio/epimethian-mcp\n```\n\nVerify the installation:\n\n```bash\nepimethian-mcp --version 2>/dev/null || which epimethian-mcp\n```\n\n## Step 2: Resolve the absolute path\n\nIDE-hosted agents may not resolve PATH correctly. Always use the absolute path in MCP configuration:\n\n```bash\nwhich epimethian-mcp\n```\n\nUse the output as the `command` value in the MCP config below.\n\n## Step 3: Collect configuration\n\nAsk the user for:\n1. **Profile name** \u2014 a short identifier for this Confluence instance (e.g., `jambit`, `acme-corp`). Lowercase alphanumeric and hyphens only.\n2. **Confluence Cloud URL** \u2014 e.g., `https://yoursite.atlassian.net`\n3. **Email address** \u2014 the email associated with their Atlassian account\n\n## Step 4: Write MCP configuration\n\nAdd the server to `.mcp.json` (or the equivalent config file for the user\'s MCP client):\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path from Step 2>",\n "env": {\n "CONFLUENCE_PROFILE": "<profile name from Step 3>"\n }\n }\n }\n}\n```\n\n**IMPORTANT:** The only env var needed is `CONFLUENCE_PROFILE`. The URL, email, and API token are stored securely in the OS keychain \u2014 they should NOT appear in config files.\n\n## Step 5: Credential setup\n\nTell the user to run this command in their terminal:\n\n```\nepimethian-mcp setup --profile <profile name from Step 3>\n```\n\nThis interactive command will:\n1. Prompt for the Confluence URL, email, and API token (masked input)\n2. Test the connection\n3. Store all credentials securely in the OS keychain under the named profile\n\nThe API token is generated at: https://id.atlassian.com/manage-profile/security/api-tokens\n\n**Do NOT ask the user for the API token yourself.** The token must go directly from the user into the interactive setup command to avoid appearing in conversation logs.\n\n## Step 6: User must restart the MCP client\n\n**IMPORTANT:** The user must restart their MCP client (e.g., restart Claude Code, reload VS Code, restart Claude Desktop) for the new server configuration to take effect. The MCP client reads `.mcp.json` at startup and does not detect changes while running.\n\nTell the user:\n> Please restart your MCP client now to activate the Confluence tools.\n\n## Step 7: Validation\n\nAfter the user restarts, verify the server is working by listing available Confluence tools or running a simple operation like listing spaces.\n\n## Adding Additional Tenants\n\nTo add a second Confluence instance (e.g., for a different customer):\n\n1. Run `epimethian-mcp setup --profile <new-profile-name>` with the new credentials\n2. In the project that uses the new tenant, update `.mcp.json` to set `CONFLUENCE_PROFILE` to the new profile name\n3. Restart the MCP client\n\nEach VS Code window / Claude Code session uses the profile specified in its `.mcp.json`. Profiles are fully isolated \u2014 different OS keychain entries, different Confluence instances.\n\n## Managing Profiles\n\n- List all profiles: `epimethian-mcp profiles`\n- Show details: `epimethian-mcp profiles --verbose`\n- Check connection: `CONFLUENCE_PROFILE=<name> epimethian-mcp status`\n\n### Removing a Profile\n\nTo delete a profile and its credentials, run:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\n**Agents must pass `--force`** because the command normally prompts for interactive confirmation (`Remove profile "<name>" and delete its credentials? [y/N]`), which will fail in non-TTY environments like agent shell sessions. The `--force` flag skips the confirmation prompt when stdin is not a TTY.\n\nThis command:\n1. Deletes the credential entry (URL, email, API token) from the OS keychain\n2. Removes the profile from the registry at `~/.config/epimethian-mcp/profiles.json`\n3. Writes an entry to the audit log at `~/.config/epimethian-mcp/audit.log`\n\nAfter removing a profile, also remove or update any `.mcp.json` files that reference it \u2014 otherwise the MCP server will fail to start with a missing-profile error.\n\n**Errors:**\n- If the profile name is invalid (not matching lowercase alphanumeric/hyphens, 1\u201363 chars), the command exits with code 1\n- If the profile does not exist in the keychain, the keychain deletion is silently skipped \u2014 the registry entry is still removed\n\n## Accessing This Guide Post-Install\n\nOnce installed, this guide is available locally via:\n\n```bash\nepimethian-mcp agent-guide\n```\n\nThis prints the full agent guide to stdout \u2014 no web fetch required.\n\n## Uninstallation\n\nWhen a user asks to uninstall Epimethian MCP, follow these steps:\n\n### Step 1: Check for existing profiles\n\n```bash\nepimethian-mcp profiles\n```\n\n### Step 2: Ask the user about credential cleanup\n\nIf profiles exist, ask the user:\n\n> You have Epimethian profiles configured: [list the profile names]. Would you like to delete all stored credentials before uninstalling? (This removes API tokens from your OS keychain.)\n\n### Step 3: Delete credentials (if the user agrees)\n\nFor each profile the user wants removed:\n\n```bash\nepimethian-mcp profiles --remove <name> --force\n```\n\nOr to remove all profiles:\n\n```bash\nfor name in $(epimethian-mcp profiles | grep \'^ \'); do epimethian-mcp profiles --remove "$name" --force; done\n```\n\n### Step 4: Remove MCP configuration\n\nDelete the `confluence` entry (or the tenant-specific entry like `confluence-jambit`) from the project\'s `.mcp.json`.\n\n### Step 5: Uninstall the package\n\n```bash\nnpm uninstall -g @de-otio/epimethian-mcp\n```\n\n### Step 6: Restart the MCP client\n\nTell the user to restart their MCP client so it stops trying to launch the removed server.\n\n## CI/CD (No Keychain)\n\nFor environments where the OS keychain is unavailable (Docker, CI), set all three env vars directly:\n\n```json\n{\n "mcpServers": {\n "confluence": {\n "command": "<absolute path>",\n "env": {\n "CONFLUENCE_URL": "<url>",\n "CONFLUENCE_EMAIL": "<email>",\n "CONFLUENCE_API_TOKEN": "<token>"\n }\n }\n }\n}\n```\n\n**Warning:** This exposes the API token in the process environment. Use profile-based auth whenever possible.\n\n## Troubleshooting\n\nIf **npm install fails**:\n- Verify Node.js 18+ is installed: `node --version`\n- Verify npm is on PATH: `npm --version`\n- If permission errors occur, the user may need to fix their npm prefix or use a Node version manager (nvm, fnm)\n\nIf **`epimethian-mcp setup` fails**:\n- "Connection failed": Verify the Confluence URL is correct and accessible\n- "Token is invalid or expired": The user needs to generate a new API token at https://id.atlassian.com/manage-profile/security/api-tokens\n- Keychain errors on Linux: The user may need to install `libsecret` / `gnome-keyring` (`apt install libsecret-tools` or equivalent)\n\nIf **the server doesn\'t appear after restart**:\n- Verify the `.mcp.json` path is correct for the user\'s MCP client\n- Verify the `command` value is an absolute path (run `which epimethian-mcp` to confirm)\n- Check that `.mcp.json` contains valid JSON (no trailing commas, correct quoting)\n\n## Available Tools (12)\n\n| Tool | Description |\n|------|-------------|\n| `create_page` | Create a new Confluence page |\n| `get_page` | Read a page by ID |\n| `get_page_by_title` | Look up a page by title in a space |\n| `update_page` | Update an existing page |\n| `delete_page` | Delete a page |\n| `list_pages` | List pages in a space |\n| `get_page_children` | Get child pages of a page |\n| `search_pages` | Search pages using CQL (Confluence Query Language) |\n| `get_spaces` | List available Confluence spaces |\n| `add_attachment` | Upload a file attachment to a page |\n| `get_attachments` | List attachments on a page |\n| `add_drawio_diagram` | Add a draw.io diagram to a page |\n';
7411
+ }
7412
+ });
7413
+
7414
+ // src/cli/agent-guide.ts
7415
+ var agent_guide_exports = {};
7416
+ __export(agent_guide_exports, {
7417
+ runAgentGuide: () => runAgentGuide
7418
+ });
7419
+ function runAgentGuide() {
7420
+ process.stdout.write(install_agent_default);
7421
+ }
7422
+ var init_agent_guide = __esm({
7423
+ "src/cli/agent-guide.ts"() {
7424
+ "use strict";
7425
+ init_install_agent();
7426
+ }
7427
+ });
7428
+
7406
7429
  // node_modules/zod/v3/external.js
7407
7430
  var external_exports = {};
7408
7431
  __export(external_exports, {
@@ -21780,15 +21803,29 @@ function sanitizeError(message) {
21780
21803
  safe = safe.replace(/Bearer [A-Za-z0-9._-]{20,}/g, "Bearer [REDACTED]");
21781
21804
  return safe;
21782
21805
  }
21806
+ var ConfluenceApiError = class extends Error {
21807
+ status;
21808
+ constructor(status, body) {
21809
+ super(`Confluence API error (${status}): ${sanitizeError(body)}`);
21810
+ this.name = "ConfluenceApiError";
21811
+ this.status = status;
21812
+ }
21813
+ };
21814
+ var ConfluenceConflictError = class extends Error {
21815
+ constructor(pageId) {
21816
+ super(
21817
+ `Version conflict: page ${pageId} has been modified since you last read it. Call get_page to fetch the latest version, then retry your update with the new version number.`
21818
+ );
21819
+ this.name = "ConfluenceConflictError";
21820
+ }
21821
+ };
21783
21822
  async function confluenceRequest(url, options = {}) {
21784
21823
  const cfg = await getConfig();
21785
21824
  const res = await fetch(url, { headers: cfg.jsonHeaders, ...options });
21786
21825
  if (!res.ok) {
21787
21826
  const body = await res.text();
21788
- console.error(`Confluence API error (${res.status}): ${body}`);
21789
- throw new Error(
21790
- `Confluence API error (${res.status}): ${sanitizeError(body)}`
21791
- );
21827
+ console.error(`Confluence API error (${res.status}): ${sanitizeError(body)}`);
21828
+ throw new ConfluenceApiError(res.status, body);
21792
21829
  }
21793
21830
  return res;
21794
21831
  }
@@ -21858,14 +21895,12 @@ async function createPage(spaceId, title, body, parentId) {
21858
21895
  return page;
21859
21896
  }
21860
21897
  async function updatePage(pageId, opts) {
21861
- const current = await getPage(pageId, true);
21862
- const newVersion = (current.version?.number ?? 0) + 1;
21863
- const newTitle = opts.title ?? current.title;
21898
+ const newVersion = opts.version + 1;
21864
21899
  const versionMessage = opts.versionMessage ? `${opts.versionMessage} (via Epimethian)` : "Updated by Epimethian";
21865
21900
  const payload = {
21866
21901
  id: pageId,
21867
21902
  status: "current",
21868
- title: newTitle,
21903
+ title: opts.title,
21869
21904
  version: { number: newVersion, message: versionMessage }
21870
21905
  };
21871
21906
  if (opts.body) {
@@ -21875,7 +21910,15 @@ async function updatePage(pageId, opts) {
21875
21910
  value: cleanBody + "\n" + buildAttributionFooter("updated")
21876
21911
  };
21877
21912
  }
21878
- const raw = await v2Put(`/pages/${pageId}`, payload);
21913
+ let raw;
21914
+ try {
21915
+ raw = await v2Put(`/pages/${pageId}`, payload);
21916
+ } catch (err) {
21917
+ if (err instanceof ConfluenceApiError && err.status === 409) {
21918
+ throw new ConfluenceConflictError(pageId);
21919
+ }
21920
+ throw err;
21921
+ }
21879
21922
  const page = PageSchema.parse(raw);
21880
21923
  try {
21881
21924
  await addLabel(page.id, ATTRIBUTION_LABEL);
@@ -21947,17 +21990,15 @@ async function uploadAttachment(pageId, fileData, filename, comment) {
21947
21990
  });
21948
21991
  if (!res.ok) {
21949
21992
  const body = await res.text();
21950
- console.error(`Confluence API error (${res.status}): ${body}`);
21951
- throw new Error(
21952
- `Confluence API error (${res.status}): ${sanitizeError(body)}`
21953
- );
21993
+ console.error(`Confluence API error (${res.status}): ${sanitizeError(body)}`);
21994
+ throw new ConfluenceApiError(res.status, body);
21954
21995
  }
21955
21996
  const data = UploadResultSchema.parse(await res.json());
21956
21997
  const att = data.results[0];
21957
21998
  if (!att) throw new Error("Attachment uploaded but no details returned.");
21958
21999
  return { title: att.title, id: att.id, fileSize: att.extensions?.fileSize };
21959
22000
  }
21960
- var GITHUB_URL = "https://github.com/rmyers/epimethian-mcp";
22001
+ var GITHUB_URL = "https://github.com/de-otio/epimethian-mcp";
21961
22002
  var ATTRIBUTION_LABEL = "epimethian-managed";
21962
22003
  var ATTRIBUTION_START = "<!-- epimethian-attribution-start -->";
21963
22004
  var ATTRIBUTION_END = "<!-- epimethian-attribution-end -->";
@@ -22070,20 +22111,22 @@ function registerTools(server, config2) {
22070
22111
  server.registerTool(
22071
22112
  "update_page",
22072
22113
  {
22073
- description: "Update an existing Confluence page. Auto-increments version number.",
22114
+ description: "Update an existing Confluence page. You must provide the version number from your most recent get_page call. If the page was modified by someone else since then, this will return a conflict error \u2014 re-read the page and retry.",
22074
22115
  inputSchema: {
22075
22116
  page_id: external_exports.string().describe("The Confluence page ID"),
22076
- title: external_exports.string().optional().describe("New title (omit to keep current)"),
22117
+ title: external_exports.string().describe("Page title (use the title from get_page if unchanged)"),
22118
+ version: external_exports.number().int().positive().describe("The page version number from your most recent get_page call"),
22077
22119
  body: external_exports.string().optional().describe("New body content in plain text or storage format"),
22078
22120
  version_message: external_exports.string().optional().describe("Optional version comment")
22079
22121
  },
22080
- annotations: { destructiveHint: false, idempotentHint: true }
22122
+ annotations: { destructiveHint: false, idempotentHint: false }
22081
22123
  },
22082
- async ({ page_id, title, body, version_message }) => {
22124
+ async ({ page_id, title, version: version2, body, version_message }) => {
22083
22125
  try {
22084
22126
  const { page, newVersion } = await updatePage(page_id, {
22085
22127
  title,
22086
22128
  body,
22129
+ version: version2,
22087
22130
  versionMessage: version_message
22088
22131
  });
22089
22132
  return toolResult(
@@ -22323,12 +22366,13 @@ function registerTools(server, config2) {
22323
22366
  `</ac:structured-macro>`
22324
22367
  ].join("\n");
22325
22368
  const current = await getPage(page_id, true);
22326
- const newVersion = (current.version?.number ?? 0) + 1;
22327
22369
  const existingBody = current.body?.storage?.value ?? current.body?.value ?? "";
22328
22370
  const newBody = append ? `${existingBody}
22329
22371
  ${macro}` : macro;
22330
- const { page } = await updatePage(page_id, {
22372
+ const { page, newVersion } = await updatePage(page_id, {
22373
+ title: current.title,
22331
22374
  body: newBody,
22375
+ version: current.version?.number ?? 0,
22332
22376
  versionMessage: `Added diagram: ${filename}`
22333
22377
  });
22334
22378
  return toolResult(
@@ -22398,6 +22442,9 @@ async function run() {
22398
22442
  } else if (command === "status") {
22399
22443
  const { runStatus: runStatus2 } = await Promise.resolve().then(() => (init_status(), status_exports));
22400
22444
  await runStatus2();
22445
+ } else if (command === "agent-guide") {
22446
+ const { runAgentGuide: runAgentGuide2 } = await Promise.resolve().then(() => (init_agent_guide(), agent_guide_exports));
22447
+ runAgentGuide2();
22401
22448
  } else {
22402
22449
  await main();
22403
22450
  }