@rui.branco/jira-mcp 1.7.10 → 1.7.11
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/config-store.js +553 -0
- package/index.js +492 -149
- package/package.json +3 -2
- package/setup.js +129 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rui.branco/jira-mcp",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.11",
|
|
4
4
|
"description": "Jira & Confluence MCP server for any MCP-compatible AI client (Claude Code, Codex, Antigravity, Cursor, etc.) - fetch tickets, search with JQL, update tickets, manage comments, change status, get Figma designs. Optional per-tool scopes, dry-run, audit log, and rate limiting.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.js",
|
|
11
|
-
"setup.js"
|
|
11
|
+
"setup.js",
|
|
12
|
+
"config-store.js"
|
|
12
13
|
],
|
|
13
14
|
"scripts": {
|
|
14
15
|
"setup": "node setup.js",
|
package/setup.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
const readline = require("readline");
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
+
const configStore = require("./config-store.js");
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const configPath = path.join(configDir, "config.json");
|
|
8
|
+
const configPath = configStore.configPath;
|
|
9
9
|
|
|
10
10
|
// Check for command line arguments
|
|
11
11
|
let args = process.argv.slice(2);
|
|
@@ -14,62 +14,55 @@ if (args[0] === "setup") args = args.slice(1);
|
|
|
14
14
|
|
|
15
15
|
// Load existing config if present
|
|
16
16
|
function loadConfig() {
|
|
17
|
-
|
|
18
|
-
if (fs.existsSync(configPath)) {
|
|
19
|
-
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
20
|
-
}
|
|
21
|
-
} catch {}
|
|
22
|
-
return null;
|
|
17
|
+
return configStore.loadConfig();
|
|
23
18
|
}
|
|
24
19
|
|
|
25
|
-
function saveConfig(
|
|
26
|
-
|
|
27
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
28
|
-
}
|
|
29
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
20
|
+
function saveConfig(mutator) {
|
|
21
|
+
return configStore.updateConfigSync(mutator);
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
// Non-interactive: setup add <name> <email> <token> <baseUrl> <projects>
|
|
33
25
|
if (args[0] === "add" && args.length >= 5) {
|
|
34
26
|
const [, name, email, token, baseUrl, ...projectArgs] = args;
|
|
35
27
|
const projects = projectArgs.length > 0
|
|
36
|
-
? projectArgs.join(",").split(",")
|
|
28
|
+
? configStore.normalizeProjects(projectArgs.join(",").split(","))
|
|
37
29
|
: [];
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
31
|
+
let saved;
|
|
32
|
+
try {
|
|
33
|
+
saved = saveConfig((config) => {
|
|
34
|
+
configStore.migrateLegacyConfig(config);
|
|
35
|
+
const existingIdx = config.instances.findIndex((instance) => instance.name === name);
|
|
36
|
+
const previous = existingIdx >= 0 ? config.instances[existingIdx] : {};
|
|
37
|
+
const mergedProjects = configStore.normalizeProjects([
|
|
38
|
+
...(previous.projects || []),
|
|
39
|
+
...projects,
|
|
40
|
+
]);
|
|
41
|
+
configStore.assertNoDuplicateProjectOwnership(config, name, mergedProjects);
|
|
42
|
+
const instance = {
|
|
43
|
+
...previous,
|
|
44
|
+
name,
|
|
45
|
+
email,
|
|
46
|
+
token,
|
|
47
|
+
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
48
|
+
projects: mergedProjects,
|
|
49
|
+
};
|
|
50
|
+
if (existingIdx >= 0) {
|
|
51
|
+
config.instances[existingIdx] = instance;
|
|
52
|
+
} else {
|
|
53
|
+
config.instances.push(instance);
|
|
54
|
+
}
|
|
55
|
+
if (!config.defaultInstance) config.defaultInstance = name;
|
|
56
|
+
return { instance };
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Setup failed:", error.message);
|
|
60
|
+
process.exit(1);
|
|
65
61
|
}
|
|
66
62
|
|
|
67
|
-
if (!config.defaultInstance) config.defaultInstance = name;
|
|
68
|
-
|
|
69
|
-
saveConfig(config);
|
|
70
63
|
console.log(`Instance "${name}" saved to ${configPath}`);
|
|
71
|
-
if (projects.length > 0) {
|
|
72
|
-
console.log(`Projects: ${projects.join(", ")}`);
|
|
64
|
+
if (saved.instance.projects.length > 0) {
|
|
65
|
+
console.log(`Projects: ${saved.instance.projects.join(", ")}`);
|
|
73
66
|
}
|
|
74
67
|
process.exit(0);
|
|
75
68
|
}
|
|
@@ -77,19 +70,23 @@ if (args[0] === "add" && args.length >= 5) {
|
|
|
77
70
|
// Non-interactive: setup remove <name>
|
|
78
71
|
if (args[0] === "remove" && args.length >= 2) {
|
|
79
72
|
const name = args[1];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
let removed;
|
|
74
|
+
try {
|
|
75
|
+
removed = saveConfig((config) => {
|
|
76
|
+
if (!Array.isArray(config.instances)) {
|
|
77
|
+
throw new Error("No multi-instance config found.");
|
|
78
|
+
}
|
|
79
|
+
config.instances = config.instances.filter((instance) => instance.name !== name);
|
|
80
|
+
if (config.defaultInstance === name) {
|
|
81
|
+
config.defaultInstance = config.instances[0]?.name || null;
|
|
82
|
+
}
|
|
83
|
+
return { defaultInstance: config.defaultInstance };
|
|
84
|
+
});
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(error.message);
|
|
84
87
|
process.exit(1);
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
config.instances = config.instances.filter((i) => i.name !== name);
|
|
88
|
-
if (config.defaultInstance === name) {
|
|
89
|
-
config.defaultInstance = config.instances[0]?.name || null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
saveConfig(config);
|
|
93
90
|
console.log(`Instance "${name}" removed.`);
|
|
94
91
|
process.exit(0);
|
|
95
92
|
}
|
|
@@ -97,7 +94,30 @@ if (args[0] === "remove" && args.length >= 2) {
|
|
|
97
94
|
// Non-interactive: setup <email> <token> <baseUrl> (legacy single-instance)
|
|
98
95
|
if (args.length >= 3 && args[0] !== "add" && args[0] !== "remove") {
|
|
99
96
|
const [email, token, baseUrl] = args;
|
|
100
|
-
|
|
97
|
+
try {
|
|
98
|
+
saveConfig((config) => {
|
|
99
|
+
const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
|
|
100
|
+
if (Array.isArray(config.instances)) {
|
|
101
|
+
const instance =
|
|
102
|
+
config.instances.find((candidate) => candidate.name === config.defaultInstance) ||
|
|
103
|
+
config.instances[0];
|
|
104
|
+
if (!instance) throw new Error("No Jira instances are configured.");
|
|
105
|
+
instance.email = email;
|
|
106
|
+
instance.token = token;
|
|
107
|
+
instance.baseUrl = normalizedBaseUrl;
|
|
108
|
+
} else {
|
|
109
|
+
if (config.instances !== undefined) {
|
|
110
|
+
throw new Error("Jira config has an invalid instances value.");
|
|
111
|
+
}
|
|
112
|
+
config.email = email;
|
|
113
|
+
config.token = token;
|
|
114
|
+
config.baseUrl = normalizedBaseUrl;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error("Setup failed:", error.message);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
101
121
|
console.log(`Config saved to ${configPath}`);
|
|
102
122
|
process.exit(0);
|
|
103
123
|
}
|
|
@@ -166,7 +186,10 @@ async function setup() {
|
|
|
166
186
|
const team = await pickDefaultTeam(baseUrl.replace(/\/$/, ""), authStr);
|
|
167
187
|
if (team) config.defaultTeam = team;
|
|
168
188
|
|
|
169
|
-
saveConfig(
|
|
189
|
+
saveConfig((latest) => {
|
|
190
|
+
for (const key of Object.keys(latest)) delete latest[key];
|
|
191
|
+
Object.assign(latest, config);
|
|
192
|
+
});
|
|
170
193
|
console.log(`\nConfig saved to ${configPath}`);
|
|
171
194
|
|
|
172
195
|
printFigmaStatus();
|
|
@@ -244,8 +267,12 @@ async function setInstanceTeam(config) {
|
|
|
244
267
|
const authStr = Buffer.from(`${target.email}:${target.token}`).toString("base64");
|
|
245
268
|
const team = await pickDefaultTeam(target.baseUrl, authStr);
|
|
246
269
|
if (team) {
|
|
247
|
-
|
|
248
|
-
saveConfig(
|
|
270
|
+
const targetName = target.name;
|
|
271
|
+
saveConfig((latest) => {
|
|
272
|
+
const current = latest.instances?.find((instance) => instance.name === targetName);
|
|
273
|
+
if (!current) throw new Error(`Instance "${targetName}" no longer exists.`);
|
|
274
|
+
current.defaultTeam = team;
|
|
275
|
+
});
|
|
249
276
|
console.log(`\nSaved default team "${team.name}" for instance "${target.name}".`);
|
|
250
277
|
} else {
|
|
251
278
|
console.log("No changes made.");
|
|
@@ -254,24 +281,15 @@ async function setInstanceTeam(config) {
|
|
|
254
281
|
}
|
|
255
282
|
|
|
256
283
|
async function addInstance(config) {
|
|
257
|
-
|
|
284
|
+
let legacyName = null;
|
|
285
|
+
let legacyProjects = [];
|
|
258
286
|
if (config.email && !config.instances) {
|
|
259
287
|
const oldName = await ask("Name for your existing instance (e.g., work): ");
|
|
260
288
|
const oldProjects = await ask("Project prefixes for existing instance (comma-separated, e.g., MODS,ENG): ");
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
name: oldName.trim() || "default",
|
|
264
|
-
email: config.email,
|
|
265
|
-
token: config.token,
|
|
266
|
-
baseUrl: config.baseUrl,
|
|
267
|
-
projects: oldProjects.split(",").map((p) => p.trim().toUpperCase()).filter(Boolean),
|
|
268
|
-
}],
|
|
269
|
-
defaultInstance: oldName.trim() || "default",
|
|
270
|
-
};
|
|
289
|
+
legacyName = oldName.trim() || "default";
|
|
290
|
+
legacyProjects = configStore.normalizeProjects(oldProjects.split(","));
|
|
271
291
|
}
|
|
272
292
|
|
|
273
|
-
if (!config.instances) config.instances = [];
|
|
274
|
-
|
|
275
293
|
console.log("\n--- Add New Instance ---\n");
|
|
276
294
|
console.log("To get your Jira API token:");
|
|
277
295
|
console.log("1. Go to https://id.atlassian.com/manage-profile/security/api-tokens");
|
|
@@ -283,7 +301,7 @@ async function addInstance(config) {
|
|
|
283
301
|
const token = await ask("Jira API token: ");
|
|
284
302
|
const baseUrl = await ask("Jira base URL (e.g., https://company.atlassian.net): ");
|
|
285
303
|
const projectsInput = await ask("Project prefixes (comma-separated, e.g., SIDE,FUN): ");
|
|
286
|
-
const projects = projectsInput.split(",")
|
|
304
|
+
const projects = configStore.normalizeProjects(projectsInput.split(","));
|
|
287
305
|
|
|
288
306
|
const trimmedBaseUrl = baseUrl.trim().replace(/\/$/, "");
|
|
289
307
|
const authStr = Buffer.from(`${email.trim()}:${token.trim()}`).toString("base64");
|
|
@@ -298,27 +316,49 @@ async function addInstance(config) {
|
|
|
298
316
|
};
|
|
299
317
|
if (team) instance.defaultTeam = team;
|
|
300
318
|
|
|
301
|
-
// Replace or add
|
|
302
|
-
const idx = config.instances.findIndex((i) => i.name === instance.name);
|
|
303
|
-
if (idx >= 0) {
|
|
304
|
-
config.instances[idx] = instance;
|
|
305
|
-
} else {
|
|
306
|
-
config.instances.push(instance);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (!config.defaultInstance) config.defaultInstance = instance.name;
|
|
310
|
-
|
|
311
319
|
const setDefault = await ask(`Set "${instance.name}" as default? (y/N): `);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
320
|
+
const makeDefault = setDefault.trim().toLowerCase() === "y";
|
|
321
|
+
|
|
322
|
+
const savedConfig = saveConfig((latest) => {
|
|
323
|
+
if (legacyName && !Array.isArray(latest.instances)) {
|
|
324
|
+
configStore.migrateLegacyConfig(latest, legacyName);
|
|
325
|
+
const migrated = latest.instances.find((item) => item.name === legacyName);
|
|
326
|
+
migrated.projects = configStore.normalizeProjects([
|
|
327
|
+
...(migrated.projects || []),
|
|
328
|
+
...legacyProjects,
|
|
329
|
+
]);
|
|
330
|
+
} else {
|
|
331
|
+
configStore.migrateLegacyConfig(latest);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const idx = latest.instances.findIndex((item) => item.name === instance.name);
|
|
335
|
+
const previous = idx >= 0 ? latest.instances[idx] : {};
|
|
336
|
+
const savedProjects = configStore.normalizeProjects([
|
|
337
|
+
...(previous.projects || []),
|
|
338
|
+
...instance.projects,
|
|
339
|
+
]);
|
|
340
|
+
configStore.assertNoDuplicateProjectOwnership(latest, instance.name, savedProjects);
|
|
341
|
+
const toSave = {
|
|
342
|
+
...previous,
|
|
343
|
+
...instance,
|
|
344
|
+
projects: savedProjects,
|
|
345
|
+
};
|
|
346
|
+
if (idx >= 0) {
|
|
347
|
+
latest.instances[idx] = toSave;
|
|
348
|
+
} else {
|
|
349
|
+
latest.instances.push(toSave);
|
|
350
|
+
}
|
|
351
|
+
if (!latest.defaultInstance || makeDefault) {
|
|
352
|
+
latest.defaultInstance = instance.name;
|
|
353
|
+
}
|
|
354
|
+
return latest;
|
|
355
|
+
});
|
|
315
356
|
|
|
316
|
-
saveConfig(config);
|
|
317
357
|
console.log(`\nInstance "${instance.name}" saved to ${configPath}`);
|
|
318
358
|
|
|
319
359
|
console.log("\nAll instances:");
|
|
320
|
-
for (const inst of
|
|
321
|
-
const isDefault = inst.name ===
|
|
360
|
+
for (const inst of savedConfig.instances) {
|
|
361
|
+
const isDefault = inst.name === savedConfig.defaultInstance ? " (default)" : "";
|
|
322
362
|
const projs = inst.projects?.length > 0 ? ` [${inst.projects.join(", ")}]` : "";
|
|
323
363
|
console.log(` - ${inst.name}${isDefault}: ${inst.baseUrl}${projs}`);
|
|
324
364
|
}
|