@thammarongg/jira-mcp 0.4.0 → 0.5.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 +31 -3
- package/dist/agents.js +412 -0
- package/dist/index.js +54 -3
- package/dist/install.js +47 -381
- package/dist/prompt.js +139 -0
- package/dist/status.js +81 -0
- package/dist/uninstall.js +140 -0
- package/dist/update.js +71 -0
- package/package.json +1 -1
- package/skill/SKILL.md +23 -0
package/README.md
CHANGED
|
@@ -16,9 +16,14 @@ npx -y @thammarongg/jira-mcp install
|
|
|
16
16
|
|
|
17
17
|
Shows a menu — Select All or pick agents (Claude Code, OpenCode, Codex, Cursor,
|
|
18
18
|
Claude Desktop, Gemini CLI) — then asks for the Jira base URL and credentials
|
|
19
|
-
(token entry is
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
(token entry is masked on a TTY). If a selected agent already has a `jira`
|
|
20
|
+
entry, the installer offers to reuse its base URL and account without asking
|
|
21
|
+
for the credentials again. Existing config files are backed up to `.bak`
|
|
22
|
+
before any modification, and re-running updates the `jira` entry in place
|
|
23
|
+
instead of duplicating it.
|
|
24
|
+
|
|
25
|
+
With `--yes` and no credential flags, a selected existing entry's credentials
|
|
26
|
+
are reused without prompting.
|
|
22
27
|
|
|
23
28
|
Non-interactive (CI / scripting):
|
|
24
29
|
|
|
@@ -39,6 +44,29 @@ Flags: `--agents all` or comma-separated ids/numbers (`claude-code`,
|
|
|
39
44
|
`--email` (Cloud) / `--username` (Data Center), `--token` (API token or PAT),
|
|
40
45
|
`--password` (DC app password), `--yes` (skip the confirm prompt).
|
|
41
46
|
|
|
47
|
+
### CLI commands
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx -y @thammarongg/jira-mcp update # refresh configured agents
|
|
51
|
+
npx -y @thammarongg/jira-mcp update --agents codex
|
|
52
|
+
npx -y @thammarongg/jira-mcp uninstall # choose configured agents interactively
|
|
53
|
+
npx -y @thammarongg/jira-mcp uninstall --agents all --yes
|
|
54
|
+
npx -y @thammarongg/jira-mcp --version
|
|
55
|
+
npx -y @thammarongg/jira-mcp --help
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`update` is non-interactive and repairs every existing `jira` entry from that
|
|
59
|
+
agent's own credentials; agents without an entry are skipped. `uninstall`
|
|
60
|
+
removes only the `jira` entry, preserving the rest of each agent configuration;
|
|
61
|
+
it accepts the same `--agents` selection grammar as `install` and asks for
|
|
62
|
+
confirmation unless `--yes` is supplied.
|
|
63
|
+
|
|
64
|
+
Running `npx -y @thammarongg/jira-mcp` bare in a terminal prints a status card
|
|
65
|
+
with the installed version, configured agents, and available commands. MCP
|
|
66
|
+
clients pipe stdio, so the same bare command starts the MCP server for them;
|
|
67
|
+
it expects `JIRA_*` environment variables, which is why running it bare
|
|
68
|
+
without those variables reports a missing `JIRA_BASE_URL`.
|
|
69
|
+
|
|
42
70
|
### Claude Code (manual one-liner)
|
|
43
71
|
|
|
44
72
|
```bash
|
package/dist/agents.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
function packageJsonPath() {
|
|
6
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
7
|
+
}
|
|
8
|
+
function readPackage() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(packageJsonPath(), "utf8"));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function readPackageName() {
|
|
17
|
+
const name = readPackage().name;
|
|
18
|
+
return typeof name === "string" && name.length > 0 ? name : "@thammarongg/jira-mcp";
|
|
19
|
+
}
|
|
20
|
+
export function readPackageVersion() {
|
|
21
|
+
const version = readPackage().version;
|
|
22
|
+
return typeof version === "string" && version.length > 0 ? version : "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
export const PKG = readPackageName();
|
|
25
|
+
export const PACKAGE_VERSION = readPackageVersion();
|
|
26
|
+
export const claudeDesktopPath = platform() === "darwin"
|
|
27
|
+
? join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
28
|
+
: join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
29
|
+
export const AGENTS = [
|
|
30
|
+
{ id: "claude-code", label: "Claude Code", path: join(homedir(), ".claude.json"), kind: "mcpServers" },
|
|
31
|
+
{ id: "opencode", label: "OpenCode", path: join(homedir(), ".config", "opencode", "opencode.json"), kind: "opencode" },
|
|
32
|
+
{ id: "codex", label: "Codex", path: join(homedir(), ".codex", "config.toml"), kind: "codex" },
|
|
33
|
+
{ id: "cursor", label: "Cursor", path: join(homedir(), ".cursor", "mcp.json"), kind: "mcpServers" },
|
|
34
|
+
{ id: "claude-desktop", label: "Claude Desktop", path: claudeDesktopPath, kind: "mcpServers" },
|
|
35
|
+
{ id: "gemini-cli", label: "Gemini CLI", path: join(homedir(), ".gemini", "settings.json"), kind: "mcpServers" },
|
|
36
|
+
];
|
|
37
|
+
export function envOf(entry) {
|
|
38
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
39
|
+
return undefined;
|
|
40
|
+
const record = entry;
|
|
41
|
+
const env = record.env ?? record.environment;
|
|
42
|
+
if (!env || typeof env !== "object" || Array.isArray(env))
|
|
43
|
+
return undefined;
|
|
44
|
+
return env;
|
|
45
|
+
}
|
|
46
|
+
function readJsonObject(path) {
|
|
47
|
+
if (!existsSync(path))
|
|
48
|
+
return undefined;
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
52
|
+
return undefined;
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function asRecord(value) {
|
|
60
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
const JIRA_HEADER = /^\s*\[mcp_servers\.jira\](\s+#.*)?$/;
|
|
63
|
+
function tomlLines(raw) {
|
|
64
|
+
const eol = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
65
|
+
const lines = raw.split("\n").map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
|
|
66
|
+
return { lines, eol };
|
|
67
|
+
}
|
|
68
|
+
function sectionEnd(lines, start) {
|
|
69
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
70
|
+
if (/^\s*\[/.test(lines[i]))
|
|
71
|
+
return i;
|
|
72
|
+
}
|
|
73
|
+
return lines.length;
|
|
74
|
+
}
|
|
75
|
+
function parseTomlString(raw, start) {
|
|
76
|
+
if (raw[start] !== '"')
|
|
77
|
+
return undefined;
|
|
78
|
+
let value = "";
|
|
79
|
+
for (let i = start + 1; i < raw.length; i++) {
|
|
80
|
+
const ch = raw[i];
|
|
81
|
+
if (ch === '"')
|
|
82
|
+
return { value, next: i + 1 };
|
|
83
|
+
if (ch !== "\\") {
|
|
84
|
+
value += ch;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const escaped = raw[++i];
|
|
88
|
+
if (escaped === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
if (escaped === "n")
|
|
91
|
+
value += "\n";
|
|
92
|
+
else if (escaped === "r")
|
|
93
|
+
value += "\r";
|
|
94
|
+
else if (escaped === "t")
|
|
95
|
+
value += "\t";
|
|
96
|
+
else if (escaped === "u") {
|
|
97
|
+
const code = raw.slice(i + 1, i + 5);
|
|
98
|
+
if (!/^[0-9a-f]{4}$/i.test(code))
|
|
99
|
+
return undefined;
|
|
100
|
+
value += String.fromCodePoint(Number.parseInt(code, 16));
|
|
101
|
+
i += 4;
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
value += escaped;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
function parseTomlEnvValue(raw) {
|
|
109
|
+
const out = {};
|
|
110
|
+
let i = 0;
|
|
111
|
+
while (i < raw.length) {
|
|
112
|
+
while (i < raw.length && /[\s,]/.test(raw[i]))
|
|
113
|
+
i++;
|
|
114
|
+
if (i >= raw.length)
|
|
115
|
+
break;
|
|
116
|
+
const keyStart = i;
|
|
117
|
+
while (i < raw.length && raw[i] !== "=" && raw[i] !== ",")
|
|
118
|
+
i++;
|
|
119
|
+
const key = raw.slice(keyStart, i).trim();
|
|
120
|
+
if (raw[i] !== "=")
|
|
121
|
+
break;
|
|
122
|
+
i++;
|
|
123
|
+
while (i < raw.length && /\s/.test(raw[i]))
|
|
124
|
+
i++;
|
|
125
|
+
let value;
|
|
126
|
+
if (raw[i] === '"') {
|
|
127
|
+
const parsed = parseTomlString(raw, i);
|
|
128
|
+
if (!parsed)
|
|
129
|
+
break;
|
|
130
|
+
value = parsed.value;
|
|
131
|
+
i = parsed.next;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const valueStart = i;
|
|
135
|
+
while (i < raw.length && raw[i] !== ",")
|
|
136
|
+
i++;
|
|
137
|
+
value = raw.slice(valueStart, i).trim();
|
|
138
|
+
}
|
|
139
|
+
if (key)
|
|
140
|
+
out[key] = value;
|
|
141
|
+
while (i < raw.length && raw[i] !== ",")
|
|
142
|
+
i++;
|
|
143
|
+
if (raw[i] === ",")
|
|
144
|
+
i++;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function parseTomlEnv(lines, start, end) {
|
|
149
|
+
for (let i = start + 1; i < end; i++) {
|
|
150
|
+
const match = lines[i].match(/^\s*env\s*=\s*\{(.*)\}\s*$/);
|
|
151
|
+
if (match)
|
|
152
|
+
return parseTomlEnvValue(match[1]);
|
|
153
|
+
}
|
|
154
|
+
return {};
|
|
155
|
+
}
|
|
156
|
+
function readCodexJira(path) {
|
|
157
|
+
if (!existsSync(path))
|
|
158
|
+
return undefined;
|
|
159
|
+
try {
|
|
160
|
+
const { lines } = tomlLines(readFileSync(path, "utf8"));
|
|
161
|
+
for (let i = 0; i < lines.length; i++) {
|
|
162
|
+
if (!JIRA_HEADER.test(lines[i]))
|
|
163
|
+
continue;
|
|
164
|
+
const end = sectionEnd(lines, i);
|
|
165
|
+
return { start: i, end, env: parseTomlEnv(lines, i, end) };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch { }
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
export function readAgentEntry(agent) {
|
|
172
|
+
if (agent.kind === "codex") {
|
|
173
|
+
const section = readCodexJira(agent.path);
|
|
174
|
+
return section ? { env: section.env } : undefined;
|
|
175
|
+
}
|
|
176
|
+
const object = readJsonObject(agent.path);
|
|
177
|
+
if (!object)
|
|
178
|
+
return undefined;
|
|
179
|
+
const container = asRecord(object[agent.kind === "opencode" ? "mcp" : "mcpServers"]);
|
|
180
|
+
return container && Object.hasOwn(container, "jira") ? container.jira : undefined;
|
|
181
|
+
}
|
|
182
|
+
export function hasAgentEntry(agent) {
|
|
183
|
+
if (agent.kind === "codex")
|
|
184
|
+
return readCodexJira(agent.path) !== undefined;
|
|
185
|
+
const object = readJsonObject(agent.path);
|
|
186
|
+
if (!object)
|
|
187
|
+
return false;
|
|
188
|
+
const container = asRecord(object[agent.kind === "opencode" ? "mcp" : "mcpServers"]);
|
|
189
|
+
return container !== undefined && Object.hasOwn(container, "jira");
|
|
190
|
+
}
|
|
191
|
+
export function readAgentEnv(agent) {
|
|
192
|
+
const env = envOf(readAgentEntry(agent));
|
|
193
|
+
if (!env)
|
|
194
|
+
return undefined;
|
|
195
|
+
const result = {};
|
|
196
|
+
for (const [key, value] of Object.entries(env)) {
|
|
197
|
+
if (typeof value === "string")
|
|
198
|
+
result[key] = value;
|
|
199
|
+
}
|
|
200
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
201
|
+
}
|
|
202
|
+
export function resolveAgents(spec) {
|
|
203
|
+
const norm = spec.trim().toLowerCase();
|
|
204
|
+
if (norm === "all" || norm === "a")
|
|
205
|
+
return [...AGENTS];
|
|
206
|
+
const ids = norm.split(/[,\s]+/).filter(Boolean);
|
|
207
|
+
const selected = [];
|
|
208
|
+
for (const id of ids) {
|
|
209
|
+
const agent = /^\d+$/.test(id) ? AGENTS[Number(id) - 1] : AGENTS.find((candidate) => candidate.id === id);
|
|
210
|
+
if (!agent)
|
|
211
|
+
throw new Error(`Unknown agent '${id}'. Valid: ${AGENTS.map((a, i) => `${i + 1}/${a.id}`).join(", ")} or all`);
|
|
212
|
+
if (!selected.includes(agent))
|
|
213
|
+
selected.push(agent);
|
|
214
|
+
}
|
|
215
|
+
if (selected.length === 0)
|
|
216
|
+
throw new Error("No agents selected");
|
|
217
|
+
return selected;
|
|
218
|
+
}
|
|
219
|
+
function reportEnvChanges(oldEnv, newEnv, path) {
|
|
220
|
+
if (!oldEnv)
|
|
221
|
+
return;
|
|
222
|
+
const replaced = Object.keys(newEnv).filter((key) => oldEnv[key] !== newEnv[key]);
|
|
223
|
+
const removed = Object.keys(oldEnv).filter((key) => !(key in newEnv));
|
|
224
|
+
if (replaced.length > 0)
|
|
225
|
+
console.log(` ! ${path}: existing jira entry will be replaced — env vars changed: ${replaced.join(", ")}`);
|
|
226
|
+
if (removed.length > 0)
|
|
227
|
+
console.log(` ! ${path}: existing jira entry will be replaced — env vars removed: ${removed.join(", ")}`);
|
|
228
|
+
}
|
|
229
|
+
function fileStamp(path) {
|
|
230
|
+
const stat = statSync(path);
|
|
231
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
232
|
+
}
|
|
233
|
+
function assertUnchanged(path, before) {
|
|
234
|
+
const after = statSync(path);
|
|
235
|
+
if (after.mtimeMs !== before.mtimeMs || after.size !== before.size) {
|
|
236
|
+
throw new Error("changed while installing — close running agents and retry");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function atomicWrite(path, content) {
|
|
240
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
241
|
+
writeFileSync(tmp, content);
|
|
242
|
+
renameSync(tmp, path);
|
|
243
|
+
}
|
|
244
|
+
function mergeJsonFile(path, mutate) {
|
|
245
|
+
const existed = existsSync(path);
|
|
246
|
+
let stamp;
|
|
247
|
+
let object = {};
|
|
248
|
+
if (existed) {
|
|
249
|
+
stamp = fileStamp(path);
|
|
250
|
+
const raw = readFileSync(path, "utf8");
|
|
251
|
+
let parsed;
|
|
252
|
+
try {
|
|
253
|
+
parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
parsed = undefined;
|
|
257
|
+
}
|
|
258
|
+
if (parsed === undefined || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
259
|
+
console.log(` ! ${path} is not a JSON object — backing up to ${path}.bak and starting fresh`);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
object = parsed;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
mutate(object);
|
|
266
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
267
|
+
if (existed) {
|
|
268
|
+
assertUnchanged(path, stamp);
|
|
269
|
+
copyFileSync(path, `${path}.bak`);
|
|
270
|
+
}
|
|
271
|
+
atomicWrite(path, JSON.stringify(object, null, 2) + "\n");
|
|
272
|
+
return existed ? "updated" : "created";
|
|
273
|
+
}
|
|
274
|
+
function tomlStr(value) {
|
|
275
|
+
let output = "";
|
|
276
|
+
for (const ch of value) {
|
|
277
|
+
const code = ch.codePointAt(0);
|
|
278
|
+
if (ch === "\\")
|
|
279
|
+
output += "\\\\";
|
|
280
|
+
else if (ch === '"')
|
|
281
|
+
output += '\\"';
|
|
282
|
+
else if (ch === "\n")
|
|
283
|
+
output += "\\n";
|
|
284
|
+
else if (ch === "\t")
|
|
285
|
+
output += "\\t";
|
|
286
|
+
else if (ch === "\r")
|
|
287
|
+
output += "\\r";
|
|
288
|
+
else if (code < 0x20)
|
|
289
|
+
output += `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
290
|
+
else
|
|
291
|
+
output += ch;
|
|
292
|
+
}
|
|
293
|
+
return `"${output}"`;
|
|
294
|
+
}
|
|
295
|
+
function upsertCodex(path, env) {
|
|
296
|
+
const block = [
|
|
297
|
+
"[mcp_servers.jira]",
|
|
298
|
+
'command = "npx"',
|
|
299
|
+
`args = ["-y", ${tomlStr(PKG)}]`,
|
|
300
|
+
`env = { ${Object.entries(env)
|
|
301
|
+
.map(([key, value]) => `${key} = ${tomlStr(value)}`)
|
|
302
|
+
.join(", ")} }`,
|
|
303
|
+
];
|
|
304
|
+
if (!existsSync(path)) {
|
|
305
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
306
|
+
atomicWrite(path, block.join("\n") + "\n");
|
|
307
|
+
return "created";
|
|
308
|
+
}
|
|
309
|
+
const stamp = fileStamp(path);
|
|
310
|
+
const raw = readFileSync(path, "utf8");
|
|
311
|
+
const { lines, eol } = tomlLines(raw);
|
|
312
|
+
const starts = [];
|
|
313
|
+
for (let i = 0; i < lines.length; i++) {
|
|
314
|
+
if (JIRA_HEADER.test(lines[i]))
|
|
315
|
+
starts.push(i);
|
|
316
|
+
}
|
|
317
|
+
let content;
|
|
318
|
+
if (starts.length === 0) {
|
|
319
|
+
content = raw.replace(/\s*$/, "") + eol + eol + block.join(eol) + eol;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
const remove = new Set();
|
|
323
|
+
for (const start of starts) {
|
|
324
|
+
const end = sectionEnd(lines, start);
|
|
325
|
+
for (let i = start; i < end; i++)
|
|
326
|
+
remove.add(i);
|
|
327
|
+
}
|
|
328
|
+
const kept = lines.filter((_, index) => !remove.has(index));
|
|
329
|
+
let insertIndex = 0;
|
|
330
|
+
for (let i = 0; i < starts[0]; i++)
|
|
331
|
+
if (!remove.has(i))
|
|
332
|
+
insertIndex++;
|
|
333
|
+
kept.splice(insertIndex, 0, ...block);
|
|
334
|
+
const after = insertIndex + block.length;
|
|
335
|
+
if (after < kept.length && kept[after].trim() !== "")
|
|
336
|
+
kept.splice(after, 0, "");
|
|
337
|
+
content = kept.join(eol);
|
|
338
|
+
if (!content.endsWith(eol))
|
|
339
|
+
content += eol;
|
|
340
|
+
}
|
|
341
|
+
reportEnvChanges(starts.length > 0 ? parseTomlEnv(lines, starts[0], sectionEnd(lines, starts[0])) : undefined, env, path);
|
|
342
|
+
assertUnchanged(path, stamp);
|
|
343
|
+
copyFileSync(path, `${path}.bak`);
|
|
344
|
+
atomicWrite(path, content);
|
|
345
|
+
return "updated";
|
|
346
|
+
}
|
|
347
|
+
export function writeAgentConfig(agent, env) {
|
|
348
|
+
if (agent.kind === "codex")
|
|
349
|
+
return upsertCodex(agent.path, env);
|
|
350
|
+
if (agent.kind === "opencode") {
|
|
351
|
+
const jsonc = agent.path.replace(/\.json$/, ".jsonc");
|
|
352
|
+
if (existsSync(jsonc)) {
|
|
353
|
+
console.log(` ! ${jsonc} exists — skipping automatic opencode write (JSONC may contain comments); merge the jira entry manually`);
|
|
354
|
+
return "skipped";
|
|
355
|
+
}
|
|
356
|
+
return mergeJsonFile(agent.path, (object) => {
|
|
357
|
+
const mcp = (object.mcp ??= {});
|
|
358
|
+
reportEnvChanges(envOf(mcp.jira), env, agent.path);
|
|
359
|
+
mcp.jira = {
|
|
360
|
+
type: "local",
|
|
361
|
+
command: ["npx", "-y", PKG],
|
|
362
|
+
environment: env,
|
|
363
|
+
enabled: true,
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return mergeJsonFile(agent.path, (object) => {
|
|
368
|
+
const mcpServers = (object.mcpServers ??= {});
|
|
369
|
+
reportEnvChanges(envOf(mcpServers.jira), env, agent.path);
|
|
370
|
+
mcpServers.jira = { command: "npx", args: ["-y", PKG], env };
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function removeJsonEntry(agent) {
|
|
374
|
+
if (!hasAgentEntry(agent))
|
|
375
|
+
return "skipped";
|
|
376
|
+
mergeJsonFile(agent.path, (object) => {
|
|
377
|
+
const key = agent.kind === "opencode" ? "mcp" : "mcpServers";
|
|
378
|
+
const container = asRecord(object[key]);
|
|
379
|
+
if (container)
|
|
380
|
+
delete container.jira;
|
|
381
|
+
});
|
|
382
|
+
return "removed";
|
|
383
|
+
}
|
|
384
|
+
function removeCodex(path) {
|
|
385
|
+
if (!existsSync(path))
|
|
386
|
+
return "skipped";
|
|
387
|
+
const stamp = fileStamp(path);
|
|
388
|
+
const raw = readFileSync(path, "utf8");
|
|
389
|
+
const { lines, eol } = tomlLines(raw);
|
|
390
|
+
const starts = [];
|
|
391
|
+
for (let i = 0; i < lines.length; i++)
|
|
392
|
+
if (JIRA_HEADER.test(lines[i]))
|
|
393
|
+
starts.push(i);
|
|
394
|
+
if (starts.length === 0)
|
|
395
|
+
return "skipped";
|
|
396
|
+
const remove = new Set();
|
|
397
|
+
for (const start of starts) {
|
|
398
|
+
const end = sectionEnd(lines, start);
|
|
399
|
+
for (let i = start; i < end; i++)
|
|
400
|
+
remove.add(i);
|
|
401
|
+
}
|
|
402
|
+
const content = lines.filter((_, index) => !remove.has(index)).join(eol);
|
|
403
|
+
assertUnchanged(path, stamp);
|
|
404
|
+
copyFileSync(path, `${path}.bak`);
|
|
405
|
+
atomicWrite(path, content);
|
|
406
|
+
return "removed";
|
|
407
|
+
}
|
|
408
|
+
export function removeAgentConfig(agent) {
|
|
409
|
+
if (agent.kind === "codex")
|
|
410
|
+
return removeCodex(agent.path);
|
|
411
|
+
return removeJsonEntry(agent);
|
|
412
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,14 +12,62 @@ import { registerUserTools } from "./tools/users.js";
|
|
|
12
12
|
import { registerMetaTools } from "./tools/meta.js";
|
|
13
13
|
import { registerGenericTools } from "./tools/generic.js";
|
|
14
14
|
import { runInstaller } from "./install.js";
|
|
15
|
+
import { runUpdate } from "./update.js";
|
|
16
|
+
import { runUninstall } from "./uninstall.js";
|
|
17
|
+
import { PACKAGE_VERSION } from "./agents.js";
|
|
18
|
+
import { renderStatus } from "./status.js";
|
|
19
|
+
function helpText() {
|
|
20
|
+
return `jira-mcp — Jira MCP server and configuration helper
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
install Configure the jira MCP server in one or more agents
|
|
24
|
+
update Refresh existing jira entries from their own credentials
|
|
25
|
+
uninstall Remove jira entries from selected agents
|
|
26
|
+
--version, -v Print the installed version
|
|
27
|
+
--help, -h Show this help
|
|
28
|
+
|
|
29
|
+
With no arguments in a terminal, jira-mcp prints a configuration status card.
|
|
30
|
+
With no arguments over piped stdio, it starts the MCP server.
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
33
|
+
function printHelp() {
|
|
34
|
+
console.log(helpText());
|
|
35
|
+
}
|
|
15
36
|
async function main() {
|
|
16
|
-
|
|
37
|
+
const command = process.argv[2];
|
|
38
|
+
if (command === "install") {
|
|
17
39
|
await runInstaller(process.argv.slice(3));
|
|
18
40
|
return;
|
|
19
41
|
}
|
|
42
|
+
if (command === "update") {
|
|
43
|
+
await runUpdate(process.argv.slice(3));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (command === "uninstall") {
|
|
47
|
+
await runUninstall(process.argv.slice(3));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (command === "--version" || command === "-v") {
|
|
51
|
+
console.log(PACKAGE_VERSION);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (command === "--help" || command === "-h") {
|
|
55
|
+
printHelp();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (command !== undefined) {
|
|
59
|
+
console.error(`Unknown command: ${command}`);
|
|
60
|
+
console.error(helpText());
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!command && process.stdin.isTTY) {
|
|
65
|
+
console.log(await renderStatus());
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
20
68
|
const config = loadConfig();
|
|
21
69
|
const client = new JiraClient(config);
|
|
22
|
-
const server = new McpServer({ name: "jira", version:
|
|
70
|
+
const server = new McpServer({ name: "jira", version: PACKAGE_VERSION });
|
|
23
71
|
registerBoardTools(server, client);
|
|
24
72
|
registerSprintTools(server, client);
|
|
25
73
|
registerEpicTools(server, client);
|
|
@@ -34,7 +82,10 @@ async function main() {
|
|
|
34
82
|
}
|
|
35
83
|
main().catch((err) => {
|
|
36
84
|
const msg = err instanceof Error ? err.message : String(err);
|
|
37
|
-
const
|
|
85
|
+
const command = process.argv[2];
|
|
86
|
+
const prefix = command === "install" || command === "update" || command === "uninstall"
|
|
87
|
+
? `jira-mcp ${command} failed`
|
|
88
|
+
: "jira-mcp failed to start";
|
|
38
89
|
console.error(`${prefix}: ${msg}`);
|
|
39
90
|
process.exit(1);
|
|
40
91
|
});
|