@de-otio/epimethian-mcp 3.0.1 → 4.0.1
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 +20 -1
- package/dist/cli/index.js +67 -20
- package/dist/cli/index.js.map +4 -4
- package/package.json +1 -1
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
|
-
|
|
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,6 +24,8 @@ 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
|
|
@@ -57,8 +73,11 @@ Manage profiles:
|
|
|
57
73
|
epimethian-mcp profiles # list all
|
|
58
74
|
epimethian-mcp profiles --verbose # show URLs and emails
|
|
59
75
|
CONFLUENCE_PROFILE=jambit epimethian-mcp status # test connection
|
|
76
|
+
epimethian-mcp profiles --remove <name> # delete profile and credentials
|
|
60
77
|
```
|
|
61
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
|
+
|
|
62
81
|
## Tools
|
|
63
82
|
|
|
64
83
|
| Tool | Description |
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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,10 +21990,8 @@ 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
|
|
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];
|
|
@@ -21966,7 +22007,7 @@ function buildAttributionFooter(action) {
|
|
|
21966
22007
|
}
|
|
21967
22008
|
function stripAttributionFooter(body) {
|
|
21968
22009
|
return body.replace(
|
|
21969
|
-
|
|
22010
|
+
/<!--\s*epimethian-attribution-start\s*-->[\s\S]*?<!--\s*epimethian-attribution-end\s*-->/g,
|
|
21970
22011
|
""
|
|
21971
22012
|
).trimEnd();
|
|
21972
22013
|
}
|
|
@@ -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.
|
|
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().
|
|
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:
|
|
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
|
}
|