@thammarongg/jira-mcp 0.4.0 → 0.5.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 CHANGED
@@ -11,25 +11,30 @@ Once published to npm, any MCP client can run it via `npx` — no local build ne
11
11
  ### Interactive installer (all agents at once)
12
12
 
13
13
  ```bash
14
- npx -y @thammarongg/jira-mcp install
14
+ npx -y @thammarongg/jira-mcp@latest install
15
15
  ```
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 hidden on a TTY). Existing config files are backed up to
20
- `.bak` before any modification, and re-running updates the `jira` entry in
21
- place instead of duplicating it.
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
 
25
30
  ```bash
26
31
  # Jira Cloud
27
- npx -y @thammarongg/jira-mcp install --agents all \
32
+ npx -y @thammarongg/jira-mcp@latest install --agents all \
28
33
  --base-url https://your-org.atlassian.net \
29
34
  --email you@example.com --token xxx --yes
30
35
 
31
36
  # Jira Data Center (subset of agents)
32
- npx -y @thammarongg/jira-mcp install --agents claude-code,codex \
37
+ npx -y @thammarongg/jira-mcp@latest install --agents claude-code,codex \
33
38
  --base-url https://jira.yourcompany.com \
34
39
  --username you --token xxx --yes
35
40
  ```
@@ -39,12 +44,35 @@ 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@latest update # refresh configured agents
51
+ npx -y @thammarongg/jira-mcp@latest update --agents codex
52
+ npx -y @thammarongg/jira-mcp@latest uninstall # choose configured agents interactively
53
+ npx -y @thammarongg/jira-mcp@latest uninstall --agents all --yes
54
+ npx -y @thammarongg/jira-mcp@latest --version
55
+ npx -y @thammarongg/jira-mcp@latest --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@latest` 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
45
73
  claude mcp add jira --env JIRA_BASE_URL=https://your-org.atlassian.net \
46
74
  --env JIRA_EMAIL=you@example.com --env JIRA_API_TOKEN=xxx \
47
- -- npx -y @thammarongg/jira-mcp
75
+ -- npx -y @thammarongg/jira-mcp@latest
48
76
  ```
49
77
 
50
78
  ```json
@@ -53,7 +81,7 @@ claude mcp add jira --env JIRA_BASE_URL=https://your-org.atlassian.net \
53
81
  "mcpServers": {
54
82
  "jira": {
55
83
  "command": "npx",
56
- "args": ["-y", "@thammarongg/jira-mcp"],
84
+ "args": ["-y", "@thammarongg/jira-mcp@latest"],
57
85
  "env": {
58
86
  "JIRA_BASE_URL": "https://your-org.atlassian.net",
59
87
  "JIRA_EMAIL": "you@example.com",
@@ -70,7 +98,7 @@ claude mcp add jira --env JIRA_BASE_URL=https://your-org.atlassian.net \
70
98
  "mcp": {
71
99
  "jira": {
72
100
  "type": "local",
73
- "command": ["npx", "-y", "@thammarongg/jira-mcp"],
101
+ "command": ["npx", "-y", "@thammarongg/jira-mcp@latest"],
74
102
  "environment": {
75
103
  "JIRA_BASE_URL": "https://your-org.atlassian.net",
76
104
  "JIRA_EMAIL": "you@example.com",
package/dist/agents.js ADDED
@@ -0,0 +1,417 @@
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
+ // An unversioned npx package spec resolves the current project when the MCP
26
+ // client starts in this repository, but this package's own bin is not linked
27
+ // into node_modules/.bin. The explicit dist-tag forces npx to resolve the
28
+ // published package in every working directory.
29
+ export const PKG_SPEC = `${PKG}@latest`;
30
+ export const PACKAGE_VERSION = readPackageVersion();
31
+ export const claudeDesktopPath = platform() === "darwin"
32
+ ? join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
33
+ : join(homedir(), ".config", "Claude", "claude_desktop_config.json");
34
+ export const AGENTS = [
35
+ { id: "claude-code", label: "Claude Code", path: join(homedir(), ".claude.json"), kind: "mcpServers" },
36
+ { id: "opencode", label: "OpenCode", path: join(homedir(), ".config", "opencode", "opencode.json"), kind: "opencode" },
37
+ { id: "codex", label: "Codex", path: join(homedir(), ".codex", "config.toml"), kind: "codex" },
38
+ { id: "cursor", label: "Cursor", path: join(homedir(), ".cursor", "mcp.json"), kind: "mcpServers" },
39
+ { id: "claude-desktop", label: "Claude Desktop", path: claudeDesktopPath, kind: "mcpServers" },
40
+ { id: "gemini-cli", label: "Gemini CLI", path: join(homedir(), ".gemini", "settings.json"), kind: "mcpServers" },
41
+ ];
42
+ export function envOf(entry) {
43
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
44
+ return undefined;
45
+ const record = entry;
46
+ const env = record.env ?? record.environment;
47
+ if (!env || typeof env !== "object" || Array.isArray(env))
48
+ return undefined;
49
+ return env;
50
+ }
51
+ function readJsonObject(path) {
52
+ if (!existsSync(path))
53
+ return undefined;
54
+ try {
55
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
56
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
57
+ return undefined;
58
+ return parsed;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ function asRecord(value) {
65
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
66
+ }
67
+ const JIRA_HEADER = /^\s*\[mcp_servers\.jira\](\s+#.*)?$/;
68
+ function tomlLines(raw) {
69
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
70
+ const lines = raw.split("\n").map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
71
+ return { lines, eol };
72
+ }
73
+ function sectionEnd(lines, start) {
74
+ for (let i = start + 1; i < lines.length; i++) {
75
+ if (/^\s*\[/.test(lines[i]))
76
+ return i;
77
+ }
78
+ return lines.length;
79
+ }
80
+ function parseTomlString(raw, start) {
81
+ if (raw[start] !== '"')
82
+ return undefined;
83
+ let value = "";
84
+ for (let i = start + 1; i < raw.length; i++) {
85
+ const ch = raw[i];
86
+ if (ch === '"')
87
+ return { value, next: i + 1 };
88
+ if (ch !== "\\") {
89
+ value += ch;
90
+ continue;
91
+ }
92
+ const escaped = raw[++i];
93
+ if (escaped === undefined)
94
+ return undefined;
95
+ if (escaped === "n")
96
+ value += "\n";
97
+ else if (escaped === "r")
98
+ value += "\r";
99
+ else if (escaped === "t")
100
+ value += "\t";
101
+ else if (escaped === "u") {
102
+ const code = raw.slice(i + 1, i + 5);
103
+ if (!/^[0-9a-f]{4}$/i.test(code))
104
+ return undefined;
105
+ value += String.fromCodePoint(Number.parseInt(code, 16));
106
+ i += 4;
107
+ }
108
+ else
109
+ value += escaped;
110
+ }
111
+ return undefined;
112
+ }
113
+ function parseTomlEnvValue(raw) {
114
+ const out = {};
115
+ let i = 0;
116
+ while (i < raw.length) {
117
+ while (i < raw.length && /[\s,]/.test(raw[i]))
118
+ i++;
119
+ if (i >= raw.length)
120
+ break;
121
+ const keyStart = i;
122
+ while (i < raw.length && raw[i] !== "=" && raw[i] !== ",")
123
+ i++;
124
+ const key = raw.slice(keyStart, i).trim();
125
+ if (raw[i] !== "=")
126
+ break;
127
+ i++;
128
+ while (i < raw.length && /\s/.test(raw[i]))
129
+ i++;
130
+ let value;
131
+ if (raw[i] === '"') {
132
+ const parsed = parseTomlString(raw, i);
133
+ if (!parsed)
134
+ break;
135
+ value = parsed.value;
136
+ i = parsed.next;
137
+ }
138
+ else {
139
+ const valueStart = i;
140
+ while (i < raw.length && raw[i] !== ",")
141
+ i++;
142
+ value = raw.slice(valueStart, i).trim();
143
+ }
144
+ if (key)
145
+ out[key] = value;
146
+ while (i < raw.length && raw[i] !== ",")
147
+ i++;
148
+ if (raw[i] === ",")
149
+ i++;
150
+ }
151
+ return out;
152
+ }
153
+ function parseTomlEnv(lines, start, end) {
154
+ for (let i = start + 1; i < end; i++) {
155
+ const match = lines[i].match(/^\s*env\s*=\s*\{(.*)\}\s*$/);
156
+ if (match)
157
+ return parseTomlEnvValue(match[1]);
158
+ }
159
+ return {};
160
+ }
161
+ function readCodexJira(path) {
162
+ if (!existsSync(path))
163
+ return undefined;
164
+ try {
165
+ const { lines } = tomlLines(readFileSync(path, "utf8"));
166
+ for (let i = 0; i < lines.length; i++) {
167
+ if (!JIRA_HEADER.test(lines[i]))
168
+ continue;
169
+ const end = sectionEnd(lines, i);
170
+ return { start: i, end, env: parseTomlEnv(lines, i, end) };
171
+ }
172
+ }
173
+ catch { }
174
+ return undefined;
175
+ }
176
+ export function readAgentEntry(agent) {
177
+ if (agent.kind === "codex") {
178
+ const section = readCodexJira(agent.path);
179
+ return section ? { env: section.env } : undefined;
180
+ }
181
+ const object = readJsonObject(agent.path);
182
+ if (!object)
183
+ return undefined;
184
+ const container = asRecord(object[agent.kind === "opencode" ? "mcp" : "mcpServers"]);
185
+ return container && Object.hasOwn(container, "jira") ? container.jira : undefined;
186
+ }
187
+ export function hasAgentEntry(agent) {
188
+ if (agent.kind === "codex")
189
+ return readCodexJira(agent.path) !== undefined;
190
+ const object = readJsonObject(agent.path);
191
+ if (!object)
192
+ return false;
193
+ const container = asRecord(object[agent.kind === "opencode" ? "mcp" : "mcpServers"]);
194
+ return container !== undefined && Object.hasOwn(container, "jira");
195
+ }
196
+ export function readAgentEnv(agent) {
197
+ const env = envOf(readAgentEntry(agent));
198
+ if (!env)
199
+ return undefined;
200
+ const result = {};
201
+ for (const [key, value] of Object.entries(env)) {
202
+ if (typeof value === "string")
203
+ result[key] = value;
204
+ }
205
+ return Object.keys(result).length > 0 ? result : undefined;
206
+ }
207
+ export function resolveAgents(spec) {
208
+ const norm = spec.trim().toLowerCase();
209
+ if (norm === "all" || norm === "a")
210
+ return [...AGENTS];
211
+ const ids = norm.split(/[,\s]+/).filter(Boolean);
212
+ const selected = [];
213
+ for (const id of ids) {
214
+ const agent = /^\d+$/.test(id) ? AGENTS[Number(id) - 1] : AGENTS.find((candidate) => candidate.id === id);
215
+ if (!agent)
216
+ throw new Error(`Unknown agent '${id}'. Valid: ${AGENTS.map((a, i) => `${i + 1}/${a.id}`).join(", ")} or all`);
217
+ if (!selected.includes(agent))
218
+ selected.push(agent);
219
+ }
220
+ if (selected.length === 0)
221
+ throw new Error("No agents selected");
222
+ return selected;
223
+ }
224
+ function reportEnvChanges(oldEnv, newEnv, path) {
225
+ if (!oldEnv)
226
+ return;
227
+ const replaced = Object.keys(newEnv).filter((key) => oldEnv[key] !== newEnv[key]);
228
+ const removed = Object.keys(oldEnv).filter((key) => !(key in newEnv));
229
+ if (replaced.length > 0)
230
+ console.log(` ! ${path}: existing jira entry will be replaced — env vars changed: ${replaced.join(", ")}`);
231
+ if (removed.length > 0)
232
+ console.log(` ! ${path}: existing jira entry will be replaced — env vars removed: ${removed.join(", ")}`);
233
+ }
234
+ function fileStamp(path) {
235
+ const stat = statSync(path);
236
+ return { mtimeMs: stat.mtimeMs, size: stat.size };
237
+ }
238
+ function assertUnchanged(path, before) {
239
+ const after = statSync(path);
240
+ if (after.mtimeMs !== before.mtimeMs || after.size !== before.size) {
241
+ throw new Error("changed while installing — close running agents and retry");
242
+ }
243
+ }
244
+ function atomicWrite(path, content) {
245
+ const tmp = `${path}.tmp-${process.pid}`;
246
+ writeFileSync(tmp, content);
247
+ renameSync(tmp, path);
248
+ }
249
+ function mergeJsonFile(path, mutate) {
250
+ const existed = existsSync(path);
251
+ let stamp;
252
+ let object = {};
253
+ if (existed) {
254
+ stamp = fileStamp(path);
255
+ const raw = readFileSync(path, "utf8");
256
+ let parsed;
257
+ try {
258
+ parsed = raw.trim() ? JSON.parse(raw) : {};
259
+ }
260
+ catch {
261
+ parsed = undefined;
262
+ }
263
+ if (parsed === undefined || typeof parsed !== "object" || Array.isArray(parsed)) {
264
+ console.log(` ! ${path} is not a JSON object — backing up to ${path}.bak and starting fresh`);
265
+ }
266
+ else {
267
+ object = parsed;
268
+ }
269
+ }
270
+ mutate(object);
271
+ mkdirSync(dirname(path), { recursive: true });
272
+ if (existed) {
273
+ assertUnchanged(path, stamp);
274
+ copyFileSync(path, `${path}.bak`);
275
+ }
276
+ atomicWrite(path, JSON.stringify(object, null, 2) + "\n");
277
+ return existed ? "updated" : "created";
278
+ }
279
+ function tomlStr(value) {
280
+ let output = "";
281
+ for (const ch of value) {
282
+ const code = ch.codePointAt(0);
283
+ if (ch === "\\")
284
+ output += "\\\\";
285
+ else if (ch === '"')
286
+ output += '\\"';
287
+ else if (ch === "\n")
288
+ output += "\\n";
289
+ else if (ch === "\t")
290
+ output += "\\t";
291
+ else if (ch === "\r")
292
+ output += "\\r";
293
+ else if (code < 0x20)
294
+ output += `\\u${code.toString(16).toUpperCase().padStart(4, "0")}`;
295
+ else
296
+ output += ch;
297
+ }
298
+ return `"${output}"`;
299
+ }
300
+ function upsertCodex(path, env) {
301
+ const block = [
302
+ "[mcp_servers.jira]",
303
+ 'command = "npx"',
304
+ `args = ["-y", ${tomlStr(PKG_SPEC)}]`,
305
+ `env = { ${Object.entries(env)
306
+ .map(([key, value]) => `${key} = ${tomlStr(value)}`)
307
+ .join(", ")} }`,
308
+ ];
309
+ if (!existsSync(path)) {
310
+ mkdirSync(dirname(path), { recursive: true });
311
+ atomicWrite(path, block.join("\n") + "\n");
312
+ return "created";
313
+ }
314
+ const stamp = fileStamp(path);
315
+ const raw = readFileSync(path, "utf8");
316
+ const { lines, eol } = tomlLines(raw);
317
+ const starts = [];
318
+ for (let i = 0; i < lines.length; i++) {
319
+ if (JIRA_HEADER.test(lines[i]))
320
+ starts.push(i);
321
+ }
322
+ let content;
323
+ if (starts.length === 0) {
324
+ content = raw.replace(/\s*$/, "") + eol + eol + block.join(eol) + eol;
325
+ }
326
+ else {
327
+ const remove = new Set();
328
+ for (const start of starts) {
329
+ const end = sectionEnd(lines, start);
330
+ for (let i = start; i < end; i++)
331
+ remove.add(i);
332
+ }
333
+ const kept = lines.filter((_, index) => !remove.has(index));
334
+ let insertIndex = 0;
335
+ for (let i = 0; i < starts[0]; i++)
336
+ if (!remove.has(i))
337
+ insertIndex++;
338
+ kept.splice(insertIndex, 0, ...block);
339
+ const after = insertIndex + block.length;
340
+ if (after < kept.length && kept[after].trim() !== "")
341
+ kept.splice(after, 0, "");
342
+ content = kept.join(eol);
343
+ if (!content.endsWith(eol))
344
+ content += eol;
345
+ }
346
+ reportEnvChanges(starts.length > 0 ? parseTomlEnv(lines, starts[0], sectionEnd(lines, starts[0])) : undefined, env, path);
347
+ assertUnchanged(path, stamp);
348
+ copyFileSync(path, `${path}.bak`);
349
+ atomicWrite(path, content);
350
+ return "updated";
351
+ }
352
+ export function writeAgentConfig(agent, env) {
353
+ if (agent.kind === "codex")
354
+ return upsertCodex(agent.path, env);
355
+ if (agent.kind === "opencode") {
356
+ const jsonc = agent.path.replace(/\.json$/, ".jsonc");
357
+ if (existsSync(jsonc)) {
358
+ console.log(` ! ${jsonc} exists — skipping automatic opencode write (JSONC may contain comments); merge the jira entry manually`);
359
+ return "skipped";
360
+ }
361
+ return mergeJsonFile(agent.path, (object) => {
362
+ const mcp = (object.mcp ??= {});
363
+ reportEnvChanges(envOf(mcp.jira), env, agent.path);
364
+ mcp.jira = {
365
+ type: "local",
366
+ command: ["npx", "-y", PKG_SPEC],
367
+ environment: env,
368
+ enabled: true,
369
+ };
370
+ });
371
+ }
372
+ return mergeJsonFile(agent.path, (object) => {
373
+ const mcpServers = (object.mcpServers ??= {});
374
+ reportEnvChanges(envOf(mcpServers.jira), env, agent.path);
375
+ mcpServers.jira = { command: "npx", args: ["-y", PKG_SPEC], env };
376
+ });
377
+ }
378
+ function removeJsonEntry(agent) {
379
+ if (!hasAgentEntry(agent))
380
+ return "skipped";
381
+ mergeJsonFile(agent.path, (object) => {
382
+ const key = agent.kind === "opencode" ? "mcp" : "mcpServers";
383
+ const container = asRecord(object[key]);
384
+ if (container)
385
+ delete container.jira;
386
+ });
387
+ return "removed";
388
+ }
389
+ function removeCodex(path) {
390
+ if (!existsSync(path))
391
+ return "skipped";
392
+ const stamp = fileStamp(path);
393
+ const raw = readFileSync(path, "utf8");
394
+ const { lines, eol } = tomlLines(raw);
395
+ const starts = [];
396
+ for (let i = 0; i < lines.length; i++)
397
+ if (JIRA_HEADER.test(lines[i]))
398
+ starts.push(i);
399
+ if (starts.length === 0)
400
+ return "skipped";
401
+ const remove = new Set();
402
+ for (const start of starts) {
403
+ const end = sectionEnd(lines, start);
404
+ for (let i = start; i < end; i++)
405
+ remove.add(i);
406
+ }
407
+ const content = lines.filter((_, index) => !remove.has(index)).join(eol);
408
+ assertUnchanged(path, stamp);
409
+ copyFileSync(path, `${path}.bak`);
410
+ atomicWrite(path, content);
411
+ return "removed";
412
+ }
413
+ export function removeAgentConfig(agent) {
414
+ if (agent.kind === "codex")
415
+ return removeCodex(agent.path);
416
+ return removeJsonEntry(agent);
417
+ }
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
- if (process.argv[2] === "install") {
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: "0.4.0" });
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 prefix = process.argv[2] === "install" ? "jira-mcp install failed" : "jira-mcp failed to start";
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
  });