@jam-mcp/server 1.4.6 → 1.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 +2 -2
- package/dist/bootstrap/host-mcp.d.ts +1 -0
- package/dist/bootstrap/host-mcp.js +14 -3
- package/dist/bootstrap/mcp-config-merger.d.ts +1 -1
- package/dist/cli/update.d.ts +74 -0
- package/dist/cli/update.js +233 -0
- package/dist/cli-entry.d.ts +1 -1
- package/dist/cli-entry.js +9 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -74,10 +74,10 @@ auth login Store Jira credentials in this user's OS secret store
|
|
|
74
74
|
runtime Show or change which JAM build this machine runs
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Written out, that is `npx --yes @jam-mcp/launcher@1.
|
|
77
|
+
Written out, that is `npx --yes @jam-mcp/launcher@1.5.0 doctor`, or just `jam
|
|
78
78
|
doctor` if you took the launcher's optional global install. Starting from
|
|
79
79
|
nothing — no install, no runtime chosen yet — use
|
|
80
|
-
`npx --yes @jam-mcp/bootstrap@1.
|
|
80
|
+
`npx --yes @jam-mcp/bootstrap@1.5.0 init` instead.
|
|
81
81
|
|
|
82
82
|
Credentials come from the process environment or this user's OS secret store —
|
|
83
83
|
never from a repository file — and never appear in logs, telemetry, or tool
|
|
@@ -54,6 +54,7 @@ export declare const defaultHostRunner: HostRunner;
|
|
|
54
54
|
export declare const persistentHostRunner: HostRunner;
|
|
55
55
|
export declare function hostRegistration(id: HostId, options?: {
|
|
56
56
|
bare?: boolean;
|
|
57
|
+
version?: string;
|
|
57
58
|
}): HostCommand | undefined;
|
|
58
59
|
/** The removal that has to precede re-registering an entry this host already has. */
|
|
59
60
|
export declare function hostUnregistration(id: HostId): HostCommand | undefined;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { LAUNCHER_PACKAGE } from "@jam-mcp/launcher";
|
|
3
|
+
import { JAM_MCP_ENTRY, LAUNCHER_PACKAGE_SPEC } from "./mcp-config-merger.js";
|
|
3
4
|
import { shellInvocation, stripPackageRunnerPath } from "./shell-command.js";
|
|
4
5
|
/**
|
|
5
6
|
* These boot a whole Node CLI, and Claude Code health-checks every configured
|
|
@@ -75,8 +76,18 @@ export function hostRegistration(id, options = {}) {
|
|
|
75
76
|
const adapter = ADAPTERS.find((a) => a.id === id);
|
|
76
77
|
if (!adapter)
|
|
77
78
|
return undefined;
|
|
78
|
-
if (!options.bare)
|
|
79
|
-
|
|
79
|
+
if (!options.bare) {
|
|
80
|
+
// `jam update` registers a version this build is not - that is the whole
|
|
81
|
+
// point of an update. Only the pin moves; the rest of the argv is the same
|
|
82
|
+
// line setup writes, so the two cannot drift into different registrations.
|
|
83
|
+
if (!options.version)
|
|
84
|
+
return adapter.register;
|
|
85
|
+
const pinned = `${LAUNCHER_PACKAGE}@${options.version}`;
|
|
86
|
+
return {
|
|
87
|
+
command: adapter.register.command,
|
|
88
|
+
args: adapter.register.args.map((arg) => (arg === LAUNCHER_PACKAGE_SPEC ? pinned : arg)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
80
91
|
const at = adapter.register.args.indexOf("--");
|
|
81
92
|
return { command: adapter.register.command, args: [...adapter.register.args.slice(0, at), ...LAUNCH_BARE] };
|
|
82
93
|
}
|
|
@@ -13,7 +13,7 @@ import { LAUNCHER_PACKAGE_SPEC } from "@jam-mcp/launcher";
|
|
|
13
13
|
export { LAUNCHER_PACKAGE_SPEC };
|
|
14
14
|
export declare const JAM_MCP_ENTRY: {
|
|
15
15
|
readonly command: "npx";
|
|
16
|
-
readonly args: readonly ["--yes", "@jam-mcp/launcher@1.
|
|
16
|
+
readonly args: readonly ["--yes", "@jam-mcp/launcher@1.5.0", "serve"];
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Recognise wiring from before the launcher existed: a hard-coded `node` path
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type HostId, type HostRunner } from "../bootstrap/host-mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* `jam update` - move this machine to the published release without redoing setup.
|
|
4
|
+
*
|
|
5
|
+
* What actually goes stale here is not a directory. JAM is launched by the
|
|
6
|
+
* host from a registration line, and that line carries an exact pin:
|
|
7
|
+
*
|
|
8
|
+
* jam: npx --yes @jam-mcp/launcher@1.4.5 serve
|
|
9
|
+
*
|
|
10
|
+
* A newer JAM on the registry changes nothing until that line moves. Right
|
|
11
|
+
* after 1.4.6 was published, this machine measured
|
|
12
|
+
* `registration: HOST_REGISTRATION_STALE (registered 1.4.5)` - the agent was
|
|
13
|
+
* still talking to the previous server. The command that fixed it was
|
|
14
|
+
* `jam setup --agent`, which also binds the project, re-detects credentials
|
|
15
|
+
* and re-plans everything else. Re-running setup is not what "update" means,
|
|
16
|
+
* so this path never calls it.
|
|
17
|
+
*
|
|
18
|
+
* The order is the same one ASC uses, and for the same reason: the new build
|
|
19
|
+
* is proven to answer *before* the registration is pointed at it. Nothing is
|
|
20
|
+
* removed first - a failed update leaves the machine running what it was
|
|
21
|
+
* running.
|
|
22
|
+
*/
|
|
23
|
+
export type UpdateStep = "install" | "verify-install" | "switch-registration" | "verify-health";
|
|
24
|
+
export declare const UPDATE_ORDER: readonly UpdateStep[];
|
|
25
|
+
export type UpdateState =
|
|
26
|
+
/** Every registration this machine has runs the published release. */
|
|
27
|
+
"CURRENT" | "UPDATE_AVAILABLE"
|
|
28
|
+
/** A registration exists whose version cannot be read - it may run anything. */
|
|
29
|
+
| "BROKEN"
|
|
30
|
+
/** The registry could not be asked. Nothing is claimed about being up to date. */
|
|
31
|
+
| "UNKNOWN";
|
|
32
|
+
export type HostPlan = {
|
|
33
|
+
id: HostId;
|
|
34
|
+
from?: string;
|
|
35
|
+
/** A bare `jam serve` entry: the version lives in the global install, not the line. */
|
|
36
|
+
bare: boolean;
|
|
37
|
+
action: "repin" | "none" | "unreadable";
|
|
38
|
+
};
|
|
39
|
+
export type JamUpdatePlan = {
|
|
40
|
+
state: UpdateState;
|
|
41
|
+
latest?: string;
|
|
42
|
+
running: string;
|
|
43
|
+
hosts: HostPlan[];
|
|
44
|
+
steps: readonly UpdateStep[];
|
|
45
|
+
detail?: string;
|
|
46
|
+
};
|
|
47
|
+
export type HostFacts = {
|
|
48
|
+
id: HostId;
|
|
49
|
+
cliAvailable: boolean;
|
|
50
|
+
hasJamEntry: boolean;
|
|
51
|
+
entryVersion?: string;
|
|
52
|
+
entryBare?: boolean;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Decide, from what was measured. Pure - it runs nothing and writes nothing.
|
|
56
|
+
*
|
|
57
|
+
* A host with no `jam` entry is left alone. Registering JAM somewhere it was
|
|
58
|
+
* never registered is adoption, not an update, and `jam setup` is where the
|
|
59
|
+
* person says they want that.
|
|
60
|
+
*/
|
|
61
|
+
export declare function planJamUpdate(input: {
|
|
62
|
+
latest?: string;
|
|
63
|
+
hosts: HostFacts[];
|
|
64
|
+
}): JamUpdatePlan;
|
|
65
|
+
export declare function updateLine(plan: JamUpdatePlan): string;
|
|
66
|
+
export type UpdateOptions = {
|
|
67
|
+
json?: boolean;
|
|
68
|
+
/** Injected by tests. Nothing here may reach a real host CLI or npm unasked. */
|
|
69
|
+
run?: HostRunner;
|
|
70
|
+
/** Injected by tests so a plan never depends on the registry. */
|
|
71
|
+
latest?: () => string | undefined;
|
|
72
|
+
hosts?: () => HostFacts[];
|
|
73
|
+
};
|
|
74
|
+
export declare function jamUpdateCommand(command: string | undefined, options?: UpdateOptions): Promise<number>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { LAUNCHER_PACKAGE, SERVER_VERSION } from "@jam-mcp/launcher";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { detectHosts, hostRegistration, hostUnregistration, } from "../bootstrap/host-mcp.js";
|
|
4
|
+
export const UPDATE_ORDER = [
|
|
5
|
+
"install",
|
|
6
|
+
"verify-install",
|
|
7
|
+
"switch-registration",
|
|
8
|
+
"verify-health",
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* Decide, from what was measured. Pure - it runs nothing and writes nothing.
|
|
12
|
+
*
|
|
13
|
+
* A host with no `jam` entry is left alone. Registering JAM somewhere it was
|
|
14
|
+
* never registered is adoption, not an update, and `jam setup` is where the
|
|
15
|
+
* person says they want that.
|
|
16
|
+
*/
|
|
17
|
+
export function planJamUpdate(input) {
|
|
18
|
+
const running = SERVER_VERSION;
|
|
19
|
+
const registered = input.hosts.filter((host) => host.cliAvailable && host.hasJamEntry);
|
|
20
|
+
if (!input.latest) {
|
|
21
|
+
return {
|
|
22
|
+
state: "UNKNOWN",
|
|
23
|
+
running,
|
|
24
|
+
hosts: [],
|
|
25
|
+
steps: [],
|
|
26
|
+
detail: "the registry could not be asked - nothing is claimed about being up to date",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const hosts = registered.map((host) => ({
|
|
30
|
+
id: host.id,
|
|
31
|
+
...(host.entryVersion ? { from: host.entryVersion } : {}),
|
|
32
|
+
bare: host.entryBare === true,
|
|
33
|
+
action: host.entryVersion === undefined
|
|
34
|
+
? "unreadable"
|
|
35
|
+
: host.entryVersion === input.latest
|
|
36
|
+
? "none"
|
|
37
|
+
: "repin",
|
|
38
|
+
}));
|
|
39
|
+
// An entry whose version cannot be read is not "current" - it is a line
|
|
40
|
+
// running something nobody measured. Say that instead of moving it silently.
|
|
41
|
+
if (hosts.some((host) => host.action === "unreadable")) {
|
|
42
|
+
return {
|
|
43
|
+
state: "BROKEN",
|
|
44
|
+
latest: input.latest,
|
|
45
|
+
running,
|
|
46
|
+
hosts,
|
|
47
|
+
steps: [...UPDATE_ORDER],
|
|
48
|
+
detail: "a registration exists whose version could not be read",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (hosts.every((host) => host.action === "none")) {
|
|
52
|
+
return { state: "CURRENT", latest: input.latest, running, hosts, steps: [] };
|
|
53
|
+
}
|
|
54
|
+
return { state: "UPDATE_AVAILABLE", latest: input.latest, running, hosts, steps: [...UPDATE_ORDER] };
|
|
55
|
+
}
|
|
56
|
+
export function updateLine(plan) {
|
|
57
|
+
switch (plan.state) {
|
|
58
|
+
case "CURRENT":
|
|
59
|
+
return `Up to date - JAM ${plan.latest} is registered.`;
|
|
60
|
+
case "UPDATE_AVAILABLE": {
|
|
61
|
+
const moving = plan.hosts.filter((host) => host.action === "repin");
|
|
62
|
+
return `Update available - ${plan.latest}: ${moving
|
|
63
|
+
.map((host) => `${host.id} runs ${host.from}`)
|
|
64
|
+
.join(", ")}`;
|
|
65
|
+
}
|
|
66
|
+
case "BROKEN":
|
|
67
|
+
case "UNKNOWN":
|
|
68
|
+
return `${plan.state}: ${plan.detail ?? "(no detail)"}`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** A process runner with room for an install. The host runner's 20s is not enough for npm. */
|
|
72
|
+
const defaultRunner = ({ command, args }) => {
|
|
73
|
+
const result = spawnSync(command, args, {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
timeout: 180_000,
|
|
76
|
+
shell: process.platform === "win32",
|
|
77
|
+
});
|
|
78
|
+
if (result.error)
|
|
79
|
+
return { status: null, failed: true, stdout: "" };
|
|
80
|
+
return { status: result.status, failed: false, stdout: result.stdout ?? "" };
|
|
81
|
+
};
|
|
82
|
+
export async function jamUpdateCommand(command, options = {}) {
|
|
83
|
+
if (command !== undefined && command !== "check" && command !== "plan") {
|
|
84
|
+
process.stderr.write(`Unknown update command: ${command}\nUsage: jam update [check|plan] [--json]\n`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
const run = options.run ?? defaultRunner;
|
|
88
|
+
const latest = (options.latest ?? (() => registryLatest(run)))();
|
|
89
|
+
const facts = (options.hosts ?? (() => detectHosts(run)))();
|
|
90
|
+
const plan = planJamUpdate({ ...(latest ? { latest } : {}), hosts: facts });
|
|
91
|
+
if (command === "check" || command === "plan") {
|
|
92
|
+
emit(plan, options);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
if (plan.steps.length === 0) {
|
|
96
|
+
emit(plan, options);
|
|
97
|
+
return plan.state === "CURRENT" ? 0 : 1;
|
|
98
|
+
}
|
|
99
|
+
return apply(plan, run, options);
|
|
100
|
+
}
|
|
101
|
+
function emit(plan, options) {
|
|
102
|
+
if (options.json)
|
|
103
|
+
process.stdout.write(`${JSON.stringify({ package: LAUNCHER_PACKAGE, ...plan }, null, 2)}\n`);
|
|
104
|
+
else
|
|
105
|
+
process.stdout.write(`${updateLine(plan)}\n`);
|
|
106
|
+
}
|
|
107
|
+
/** What the registry has. undefined when it could not be asked - never a guess. */
|
|
108
|
+
function registryLatest(run) {
|
|
109
|
+
const result = run({ command: "npm", args: ["view", LAUNCHER_PACKAGE, "version"] });
|
|
110
|
+
if (result.failed || result.status !== 0)
|
|
111
|
+
return undefined;
|
|
112
|
+
const version = result.stdout.trim();
|
|
113
|
+
return /^\d+\.\d+\.\d+/.test(version) ? version : undefined;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Does that version actually answer?
|
|
117
|
+
*
|
|
118
|
+
* `runtime status --json` is the cheapest honest question: it resolves the
|
|
119
|
+
* runtime the registered entry would resolve, and needs no bound project. A
|
|
120
|
+
* pin that cannot answer here is one the host would fail to start - which is
|
|
121
|
+
* exactly what must not be registered.
|
|
122
|
+
*/
|
|
123
|
+
function launcherAnswers(run, version, bare) {
|
|
124
|
+
const result = bare
|
|
125
|
+
? run({ command: "jam", args: ["runtime", "status", "--json"] })
|
|
126
|
+
: run({ command: "npx", args: ["--yes", `${LAUNCHER_PACKAGE}@${version}`, "runtime", "status", "--json"] });
|
|
127
|
+
if (result.failed || result.status !== 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
try {
|
|
130
|
+
const parsed = JSON.parse(result.stdout);
|
|
131
|
+
return typeof parsed.version === "string" ? parsed.version : undefined;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function apply(plan, run, options) {
|
|
138
|
+
const target = plan.latest;
|
|
139
|
+
process.stdout.write(`${updateLine(plan)}\n`);
|
|
140
|
+
const moving = plan.hosts.filter((host) => host.action !== "none");
|
|
141
|
+
// A bare entry runs the global executable, so that is what has to move.
|
|
142
|
+
// An npx pin is fetched at launch; verifying it is what "install" means there.
|
|
143
|
+
if (moving.some((host) => host.bare)) {
|
|
144
|
+
const installed = run({ command: "npm", args: ["install", "-g", `${LAUNCHER_PACKAGE}@${target}`] });
|
|
145
|
+
if (installed.failed || installed.status !== 0) {
|
|
146
|
+
process.stderr.write(`install failed: npm install -g ${LAUNCHER_PACKAGE}@${target}\n`);
|
|
147
|
+
// Nothing was re-registered. The machine still runs what it ran.
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const host of moving) {
|
|
152
|
+
const answered = launcherAnswers(run, target, host.bare);
|
|
153
|
+
if (answered !== target) {
|
|
154
|
+
process.stderr.write(`verify failed: ${target} did not answer as ${target}${answered ? ` (got ${answered})` : ""} - registration left as it is\n`);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
process.stdout.write(`verified: ${LAUNCHER_PACKAGE}@${target} answers\n`);
|
|
159
|
+
for (const host of moving) {
|
|
160
|
+
// `mcp add` over an existing entry changes nothing on Claude Code - it
|
|
161
|
+
// answers "already exists". The removal is what makes the re-pin land.
|
|
162
|
+
if (!host.bare) {
|
|
163
|
+
const remove = hostUnregistration(host.id);
|
|
164
|
+
if (remove)
|
|
165
|
+
run(remove);
|
|
166
|
+
}
|
|
167
|
+
const register = hostRegistration(host.id, { ...(host.bare ? { bare: true } : { version: target }) });
|
|
168
|
+
if (!register)
|
|
169
|
+
continue;
|
|
170
|
+
const result = run(register);
|
|
171
|
+
if (result.failed || result.status !== 0) {
|
|
172
|
+
process.stderr.write(`switch failed on ${host.id}\n`);
|
|
173
|
+
return rollback(plan, host, run);
|
|
174
|
+
}
|
|
175
|
+
process.stdout.write(`registered: ${host.id} -> ${target}\n`);
|
|
176
|
+
}
|
|
177
|
+
// Read it back. A registration JAM could not verify is never reported as done.
|
|
178
|
+
const after = (options.hosts ?? (() => detectHosts(run)))();
|
|
179
|
+
const stale = after.filter((host) => moving.some((m) => m.id === host.id) && host.entryVersion !== target);
|
|
180
|
+
if (stale.length > 0) {
|
|
181
|
+
process.stderr.write(`health: ${stale.map((host) => `${host.id} still runs ${host.entryVersion ?? "?"}`).join(", ")}\n`);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
// A registration that points at a build which cannot read Jira is not a
|
|
185
|
+
// finished update. `doctor` is the existing health axis - config, credentials
|
|
186
|
+
// and one live read - so it is asked here rather than reimplemented.
|
|
187
|
+
const health = doctorVerdict(run, target, moving.some((host) => host.bare));
|
|
188
|
+
if (health !== "ready") {
|
|
189
|
+
process.stderr.write(`doctor: ${health} - rolling back\n`);
|
|
190
|
+
const first = moving[0];
|
|
191
|
+
return first ? rollback(plan, first, run) : 1;
|
|
192
|
+
}
|
|
193
|
+
process.stdout.write(`JAM ${target} is registered, doctor ready.\n`);
|
|
194
|
+
return 0;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* What `jam doctor --json` says about the build now registered.
|
|
198
|
+
*
|
|
199
|
+
* Run through the same entry the host would run, so this measures the thing
|
|
200
|
+
* that was just registered rather than the process doing the registering.
|
|
201
|
+
*/
|
|
202
|
+
function doctorVerdict(run, version, bare) {
|
|
203
|
+
const result = bare
|
|
204
|
+
? run({ command: "jam", args: ["doctor", "--json"] })
|
|
205
|
+
: run({ command: "npx", args: ["--yes", `${LAUNCHER_PACKAGE}@${version}`, "doctor", "--json"] });
|
|
206
|
+
if (result.failed)
|
|
207
|
+
return "could not be run";
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(result.stdout);
|
|
210
|
+
return typeof parsed.status === "string" ? parsed.status : "unreadable";
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return result.status === 0 ? "ready" : "unreadable";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Put back the pin that was there. Only possible when it was readable. */
|
|
217
|
+
function rollback(plan, host, run) {
|
|
218
|
+
if (!host.from || host.bare) {
|
|
219
|
+
process.stderr.write(`Nothing to roll back to on ${host.id} - re-register with \`jam setup --agent\`.\n`);
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
const remove = hostUnregistration(host.id);
|
|
223
|
+
if (remove)
|
|
224
|
+
run(remove);
|
|
225
|
+
const back = hostRegistration(host.id, { version: host.from });
|
|
226
|
+
const result = back ? run(back) : undefined;
|
|
227
|
+
if (result && !result.failed && result.status === 0) {
|
|
228
|
+
process.stderr.write(`rolled back - ${host.id} runs ${host.from} again.\n`);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
process.stderr.write(`rollback failed on ${host.id} - re-register with \`jam setup --agent\`.\n`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
package/dist/cli-entry.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* points (notably @jam-mcp/bootstrap) can forward to exactly these commands
|
|
4
4
|
* instead of reimplementing them.
|
|
5
5
|
*/
|
|
6
|
-
export declare const USAGE = "jam - Jira Agent MCP\n\nUsage:\n jam serve Run the MCP server over stdio (default; this is what Claude Code / Codex launch)\n jam doctor Diagnose config, credentials and Jira connectivity\n jam setup [--project KEY] [--shared] [--migrate] [--non-interactive]\n Wire up this project and run doctor. Binds it to you\n alone, writing nothing to the repository; --shared\n adopts JAM for the team (project.yaml, .mcp.json)\n jam runtime Show which JAM build this machine runs\n jam runtime use package | development <path>\n Change it (writes ~/.jam/config.yaml only, never a project)\n jam auth login Store Jira credentials in this user's OS secret store\n jam auth logout Remove them again\n\nFor coding agents and scripts (stdout is JSON only, never prompts):\n jam setup --agent One shot: detect, plan, apply what is safe, verify\n jam setup plan --json Report what setup would change, changing nothing\n jam setup apply --non-interactive --json\n Execute the plan\n jam doctor --json Health check as structured output\n jam auth status --json Whether Jira credentials are configured (never their value)\n jam jira search <jql> [--scope preview|complete]\n jam jira context <KEY> [KEY...]\n jam jira full <KEY> [KEY...]\n Read Jira from the shell - the same reads the MCP\n tools do, for a session that cannot see them yet\n\nEnvironment:\n JIRA_BASE_URL https://your-site.atlassian.net\n JIRA_EMAIL Atlassian account email\n JIRA_API_TOKEN Atlassian API token\n JAM_PROJECT_KEY Jira project key, used by `jam setup`/`jam serve` when no\n .jira-agent/project.yaml exists yet\n\nCredentials and JAM_PROJECT_KEY are read from the current shell's environment\nfirst, then (on Windows) from the User environment - so a value set with\n`setx` works without opening a new terminal.\n";
|
|
6
|
+
export declare const USAGE = "jam - Jira Agent MCP\n\nUsage:\n jam serve Run the MCP server over stdio (default; this is what Claude Code / Codex launch)\n jam doctor Diagnose config, credentials and Jira connectivity\n jam setup [--project KEY] [--shared] [--migrate] [--non-interactive]\n Wire up this project and run doctor. Binds it to you\n alone, writing nothing to the repository; --shared\n adopts JAM for the team (project.yaml, .mcp.json)\n jam update Move this machine's registration to the published release\n jam update check What is registered, what is published (changes nothing)\n jam runtime Show which JAM build this machine runs\n jam runtime use package | development <path>\n Change it (writes ~/.jam/config.yaml only, never a project)\n jam auth login Store Jira credentials in this user's OS secret store\n jam auth logout Remove them again\n\nFor coding agents and scripts (stdout is JSON only, never prompts):\n jam setup --agent One shot: detect, plan, apply what is safe, verify\n jam setup plan --json Report what setup would change, changing nothing\n jam setup apply --non-interactive --json\n Execute the plan\n jam doctor --json Health check as structured output\n jam auth status --json Whether Jira credentials are configured (never their value)\n jam jira search <jql> [--scope preview|complete]\n jam jira context <KEY> [KEY...]\n jam jira full <KEY> [KEY...]\n Read Jira from the shell - the same reads the MCP\n tools do, for a session that cannot see them yet\n\nEnvironment:\n JIRA_BASE_URL https://your-site.atlassian.net\n JIRA_EMAIL Atlassian account email\n JIRA_API_TOKEN Atlassian API token\n JAM_PROJECT_KEY Jira project key, used by `jam setup`/`jam serve` when no\n .jira-agent/project.yaml exists yet\n\nCredentials and JAM_PROJECT_KEY are read from the current shell's environment\nfirst, then (on Windows) from the User environment - so a value set with\n`setx` works without opening a new terminal.\n";
|
|
7
7
|
export declare function runJamCommand(argv: string[]): Promise<number>;
|
package/dist/cli-entry.js
CHANGED
|
@@ -3,6 +3,7 @@ import { doctor } from "./cli/doctor.js";
|
|
|
3
3
|
import { showRuntime, useRuntime } from "./cli/runtime.js";
|
|
4
4
|
import { serve } from "./cli/serve.js";
|
|
5
5
|
import { setup } from "./cli/setup.js";
|
|
6
|
+
import { jamUpdateCommand } from "./cli/update.js";
|
|
6
7
|
import { runSetupWizard } from "./cli/setup-wizard.js";
|
|
7
8
|
import { reportPromptError, Ui } from "./cli/ui.js";
|
|
8
9
|
import { authStatusCommand, doctorJsonCommand, setupAgentCommand, setupApplyCommand, setupPlanCommand, } from "./cli/agent-api.js";
|
|
@@ -21,6 +22,8 @@ Usage:
|
|
|
21
22
|
Wire up this project and run doctor. Binds it to you
|
|
22
23
|
alone, writing nothing to the repository; --shared
|
|
23
24
|
adopts JAM for the team (project.yaml, .mcp.json)
|
|
25
|
+
jam update Move this machine's registration to the published release
|
|
26
|
+
jam update check What is registered, what is published (changes nothing)
|
|
24
27
|
jam runtime Show which JAM build this machine runs
|
|
25
28
|
jam runtime use package | development <path>
|
|
26
29
|
Change it (writes ~/.jam/config.yaml only, never a project)
|
|
@@ -99,6 +102,12 @@ export async function runJamCommand(argv) {
|
|
|
99
102
|
// The wizard can ask; the plain path never does.
|
|
100
103
|
return rest.includes("--non-interactive") ? setup(common) : runSetupWizard(common);
|
|
101
104
|
}
|
|
105
|
+
case "update":
|
|
106
|
+
// Not `setup` again: setup re-plans the project binding, credentials and
|
|
107
|
+
// everything else. An update moves the registration pin and nothing more.
|
|
108
|
+
return jamUpdateCommand(rest[0] === "--json" ? undefined : rest[0], {
|
|
109
|
+
json: rest.includes("--json"),
|
|
110
|
+
});
|
|
102
111
|
case "runtime": {
|
|
103
112
|
const json = rest.includes("--json");
|
|
104
113
|
if (rest[0] === "use")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jam-mcp/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "JAM (Jira Agent MCP) - agent-facing Jira access layer: MCP server, setup core, and CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jira",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"test:watch": "vitest"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@jam-mcp/launcher": "1.
|
|
44
|
+
"@jam-mcp/launcher": "1.5.0",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
46
|
"yaml": "^2.9.0",
|
|
47
47
|
"zod": "^4.4.3"
|