mcp-multi-jira 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,204 @@
1
+ import crypto from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { password as promptPassword } from "@inquirer/prompts";
5
+ import { plainTokenFilePath, tokenFilePath } from "../config/paths.js";
6
+ import { ensureDir, atomicWrite } from "../utils/fs.js";
7
+ const SERVICE_NAME = "mcp-jira";
8
+ const TOKEN_ENV = "MCP_JIRA_TOKEN_PASSWORD";
9
+ let cachedPassword = null;
10
+ async function getMasterPassword(intent) {
11
+ if (cachedPassword !== null) {
12
+ return cachedPassword;
13
+ }
14
+ if (process.env[TOKEN_ENV] !== undefined) {
15
+ cachedPassword = process.env[TOKEN_ENV];
16
+ return cachedPassword;
17
+ }
18
+ if (!process.stdin.isTTY) {
19
+ throw new Error("Encrypted token store requires a password. Set MCP_JIRA_TOKEN_PASSWORD to run non-interactively.");
20
+ }
21
+ cachedPassword = await promptPassword({
22
+ message: intent === "read"
23
+ ? "Enter master password to unlock Jira tokens"
24
+ : "Create a master password to encrypt Jira tokens",
25
+ mask: "*",
26
+ });
27
+ return cachedPassword;
28
+ }
29
+ async function loadEncryptedFile(password) {
30
+ const filePath = tokenFilePath();
31
+ try {
32
+ const raw = await fs.readFile(filePath, "utf8");
33
+ const payload = JSON.parse(raw);
34
+ if (!payload.ciphertext) {
35
+ return {};
36
+ }
37
+ const salt = Buffer.from(payload.salt, "base64");
38
+ const iv = Buffer.from(payload.iv, "base64");
39
+ const tag = Buffer.from(payload.tag, "base64");
40
+ const ciphertext = Buffer.from(payload.ciphertext, "base64");
41
+ const key = crypto.scryptSync(password, salt, 32);
42
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
43
+ decipher.setAuthTag(tag);
44
+ const decrypted = Buffer.concat([
45
+ decipher.update(ciphertext),
46
+ decipher.final(),
47
+ ]).toString("utf8");
48
+ return JSON.parse(decrypted);
49
+ }
50
+ catch (err) {
51
+ if (err.code === "ENOENT") {
52
+ return {};
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+ async function saveEncryptedFile(password, tokens) {
58
+ const salt = crypto.randomBytes(16);
59
+ const iv = crypto.randomBytes(12);
60
+ const key = crypto.scryptSync(password, salt, 32);
61
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
62
+ const plaintext = JSON.stringify(tokens);
63
+ const ciphertext = Buffer.concat([
64
+ cipher.update(plaintext, "utf8"),
65
+ cipher.final(),
66
+ ]);
67
+ const tag = cipher.getAuthTag();
68
+ const payload = {
69
+ version: 1,
70
+ salt: salt.toString("base64"),
71
+ iv: iv.toString("base64"),
72
+ tag: tag.toString("base64"),
73
+ ciphertext: ciphertext.toString("base64"),
74
+ };
75
+ await ensureDir(path.dirname(tokenFilePath()));
76
+ await atomicWrite(tokenFilePath(), JSON.stringify(payload, null, 2));
77
+ }
78
+ async function loadPlainFile() {
79
+ const filePath = plainTokenFilePath();
80
+ try {
81
+ const raw = await fs.readFile(filePath, "utf8");
82
+ return JSON.parse(raw);
83
+ }
84
+ catch (err) {
85
+ if (err.code === "ENOENT") {
86
+ return {};
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+ async function savePlainFile(tokens) {
92
+ await ensureDir(path.dirname(plainTokenFilePath()));
93
+ await atomicWrite(plainTokenFilePath(), JSON.stringify(tokens, null, 2));
94
+ }
95
+ class EncryptedFileTokenStore {
96
+ async get(alias) {
97
+ const password = await getMasterPassword("read");
98
+ const tokens = await loadEncryptedFile(password);
99
+ return tokens[alias] ?? null;
100
+ }
101
+ async set(alias, tokens) {
102
+ const password = await getMasterPassword("write");
103
+ const existing = await loadEncryptedFile(password);
104
+ existing[alias] = tokens;
105
+ await saveEncryptedFile(password, existing);
106
+ }
107
+ async remove(alias) {
108
+ const password = await getMasterPassword("read");
109
+ const existing = await loadEncryptedFile(password);
110
+ if (existing[alias]) {
111
+ delete existing[alias];
112
+ await saveEncryptedFile(password, existing);
113
+ }
114
+ }
115
+ }
116
+ class PlaintextTokenStore {
117
+ async get(alias) {
118
+ const tokens = await loadPlainFile();
119
+ return tokens[alias] ?? null;
120
+ }
121
+ async set(alias, tokens) {
122
+ const existing = await loadPlainFile();
123
+ existing[alias] = tokens;
124
+ await savePlainFile(existing);
125
+ }
126
+ async remove(alias) {
127
+ const existing = await loadPlainFile();
128
+ if (existing[alias]) {
129
+ delete existing[alias];
130
+ await savePlainFile(existing);
131
+ }
132
+ }
133
+ }
134
+ class KeytarTokenStore {
135
+ keytar;
136
+ constructor(keytar) {
137
+ this.keytar = keytar;
138
+ }
139
+ async get(alias) {
140
+ const raw = await this.keytar.getPassword(SERVICE_NAME, `tokens:${alias}`);
141
+ if (!raw) {
142
+ return null;
143
+ }
144
+ return JSON.parse(raw);
145
+ }
146
+ async set(alias, tokens) {
147
+ await this.keytar.setPassword(SERVICE_NAME, `tokens:${alias}`, JSON.stringify(tokens));
148
+ }
149
+ async remove(alias) {
150
+ await this.keytar.deletePassword(SERVICE_NAME, `tokens:${alias}`);
151
+ }
152
+ }
153
+ async function loadKeytar() {
154
+ try {
155
+ const mod = await import("keytar");
156
+ return mod.default ?? mod;
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ export async function getAuthStatusForAlias(options) {
163
+ const allowPrompt = options.allowPrompt ?? false;
164
+ if (options.storeKind === "encrypted" &&
165
+ !allowPrompt &&
166
+ process.env[TOKEN_ENV] === undefined) {
167
+ return {
168
+ status: "locked",
169
+ reason: "Encrypted token store is locked. Set MCP_JIRA_TOKEN_PASSWORD or login interactively.",
170
+ };
171
+ }
172
+ const tokens = await options.tokenStore.get(options.alias);
173
+ if (!tokens) {
174
+ return {
175
+ status: "missing",
176
+ reason: "No tokens found. Run login to authenticate this account.",
177
+ };
178
+ }
179
+ if (tokens.expiresAt < Date.now() && !tokens.refreshToken) {
180
+ return {
181
+ status: "expired",
182
+ reason: "Token expired and no refresh token available. Run login again.",
183
+ };
184
+ }
185
+ return { status: "ok" };
186
+ }
187
+ export async function createTokenStore(options) {
188
+ const useKeychain = options?.useKeychain;
189
+ let store = options?.store ?? "encrypted";
190
+ if (useKeychain) {
191
+ store = "keychain";
192
+ }
193
+ if (store === "keychain") {
194
+ const keytar = await loadKeytar();
195
+ if (!keytar) {
196
+ throw new Error("Keychain usage requested but keytar could not be loaded. Reinstall dependencies or switch token storage to plain/encrypted.");
197
+ }
198
+ return new KeytarTokenStore(keytar);
199
+ }
200
+ if (store === "plain") {
201
+ return new PlaintextTokenStore();
202
+ }
203
+ return new EncryptedFileTokenStore();
204
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ export async function ensureDir(dir) {
4
+ await fs.mkdir(dir, { recursive: true });
5
+ }
6
+ export async function atomicWrite(filePath, contents) {
7
+ const dir = path.dirname(filePath);
8
+ const tempPath = path.join(dir, `.tmp-${Date.now()}-${process.pid}`);
9
+ await fs.writeFile(tempPath, contents, "utf8");
10
+ try {
11
+ await fs.rename(tempPath, filePath);
12
+ }
13
+ catch (err) {
14
+ try {
15
+ await fs.unlink(tempPath);
16
+ }
17
+ catch {
18
+ // ignore cleanup errors
19
+ }
20
+ throw err;
21
+ }
22
+ }
23
+ export async function backupFile(filePath) {
24
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
25
+ const backupPath = `${filePath}.${timestamp}.bak`;
26
+ await fs.copyFile(filePath, backupPath);
27
+ return backupPath;
28
+ }
@@ -0,0 +1,30 @@
1
+ const debugEnabled = process.env.MCP_JIRA_DEBUG === "1" || process.env.MCP_JIRA_DEBUG === "true";
2
+ let logTarget = process.env.MCP_JIRA_LOG_STDERR === "1" ||
3
+ process.env.MCP_JIRA_LOG_STDERR === "true"
4
+ ? "stderr"
5
+ : "stdout";
6
+ export function setLogTarget(target) {
7
+ logTarget = target;
8
+ }
9
+ function logLine(message) {
10
+ if (logTarget === "stderr") {
11
+ console.error(message);
12
+ return;
13
+ }
14
+ console.log(message);
15
+ }
16
+ export function info(message) {
17
+ logLine(message);
18
+ }
19
+ export function warn(message) {
20
+ console.error(message);
21
+ }
22
+ export function error(message) {
23
+ console.error(message);
24
+ }
25
+ export function debug(message) {
26
+ if (!debugEnabled) {
27
+ return;
28
+ }
29
+ logLine(message);
30
+ }
@@ -0,0 +1,4 @@
1
+ import { readFileSync } from "node:fs";
2
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
3
+ const packageJson = JSON.parse(readFileSync(packageJsonUrl, "utf-8"));
4
+ export const PACKAGE_VERSION = packageJson.version ?? "0.0.0";
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "mcp-multi-jira",
3
+ "version": "0.1.0",
4
+ "description": "Multi-account Jira MCP server",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "mcp",
8
+ "model-context-protocol",
9
+ "modelcontextprotocol",
10
+ "jira",
11
+ "atlassian",
12
+ "confluence",
13
+ "multi-account",
14
+ "oauth",
15
+ "router",
16
+ "proxy",
17
+ "cli",
18
+ "cursor",
19
+ "claude",
20
+ "codex",
21
+ "stdio"
22
+ ],
23
+ "bin": {
24
+ "mcp-multi-jira": "dist/cli.js"
25
+ },
26
+ "module": "src/cli.ts",
27
+ "type": "module",
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "test": "bun test",
39
+ "check": "biome check . --error-on-warnings",
40
+ "fix": "ultracite fix"
41
+ },
42
+ "dependencies": {
43
+ "@iarna/toml": "^2.2.5",
44
+ "@inquirer/prompts": "^8.1.0",
45
+ "@modelcontextprotocol/sdk": "^1.25.1",
46
+ "commander": "^14.0.2",
47
+ "get-port": "^7.1.0",
48
+ "keytar": "^7.9.0",
49
+ "open": "^11.0.0",
50
+ "p-queue": "^9.0.1",
51
+ "zod": "^4.2.1"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "2.3.10",
55
+ "@types/bun": "^1.3.5",
56
+ "@types/node": "^25.0.3",
57
+ "typescript": "^5.9.3",
58
+ "ultracite": "6.4.2"
59
+ }
60
+ }