@jam-mcp/server 1.4.0 → 1.4.2
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 +55 -1
- package/dist/bootstrap/host-mcp.js +104 -5
- package/dist/bootstrap/mcp-config-merger.d.ts +1 -1
- package/dist/bootstrap/setup-apply.js +5 -0
- package/dist/bootstrap/setup-plan.d.ts +13 -1
- package/dist/bootstrap/setup-plan.js +16 -4
- package/dist/bootstrap/setup-state.d.ts +6 -0
- package/dist/bootstrap/setup-state.js +7 -1
- package/dist/cli/agent-api.d.ts +13 -2
- package/dist/cli/agent-api.js +66 -3
- package/dist/cli/setup.js +3 -1
- package/dist/index.js +0 -0
- package/dist/mcp/create-server.d.ts +7 -0
- package/dist/mcp/create-server.js +13 -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.4.
|
|
77
|
+
Written out, that is `npx --yes @jam-mcp/launcher@1.4.2 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.4.
|
|
80
|
+
`npx --yes @jam-mcp/bootstrap@1.4.2 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
|
|
@@ -18,6 +18,20 @@ export type HostState = {
|
|
|
18
18
|
cliAvailable: boolean;
|
|
19
19
|
/** Whether it already has a `jam` entry registered for this user. */
|
|
20
20
|
hasJamEntry: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The launcher pin that entry actually runs, when the listing shows it.
|
|
23
|
+
*
|
|
24
|
+
* An entry existing is not the same as an entry being current: a pin written
|
|
25
|
+
* by an older release keeps running that release's server, which serves a
|
|
26
|
+
* different set of tools. Reading only the name made a stale registration
|
|
27
|
+
* indistinguishable from a good one, and setup then reported
|
|
28
|
+
* `already_configured` over it.
|
|
29
|
+
*/
|
|
30
|
+
entryVersion?: string;
|
|
31
|
+
/** The entry is present but does not run the launcher this release registers. */
|
|
32
|
+
entryStale?: boolean;
|
|
33
|
+
/** The entry runs the global `jam` executable rather than an npx pin. */
|
|
34
|
+
entryBare?: boolean;
|
|
21
35
|
};
|
|
22
36
|
export type HostRunResult = {
|
|
23
37
|
status: number | null;
|
|
@@ -28,7 +42,11 @@ export type HostRunResult = {
|
|
|
28
42
|
/** Injected by tests. Nothing in this module may reach a real CLI unasked. */
|
|
29
43
|
export type HostRunner = (command: HostCommand) => HostRunResult;
|
|
30
44
|
export declare const defaultHostRunner: HostRunner;
|
|
31
|
-
export declare function hostRegistration(id: HostId
|
|
45
|
+
export declare function hostRegistration(id: HostId, options?: {
|
|
46
|
+
bare?: boolean;
|
|
47
|
+
}): HostCommand | undefined;
|
|
48
|
+
/** The removal that has to precede re-registering an entry this host already has. */
|
|
49
|
+
export declare function hostUnregistration(id: HostId): HostCommand | undefined;
|
|
32
50
|
/**
|
|
33
51
|
* Is `jam` in this listing?
|
|
34
52
|
*
|
|
@@ -42,6 +60,42 @@ export declare function hostRegistration(id: HostId): HostCommand | undefined;
|
|
|
42
60
|
* `mcp add` on an existing entry writes the same launcher line back.
|
|
43
61
|
*/
|
|
44
62
|
export declare function listsJamEntry(stdout: string): boolean;
|
|
63
|
+
/** The listing line for `jam`, ANSI stripped, or null when there is none. */
|
|
64
|
+
export declare function jamEntryLine(stdout: string): string | null;
|
|
65
|
+
/** The launcher version a listing line runs, when the line names one. */
|
|
66
|
+
export declare function entryLauncherVersion(line: string): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Does this entry run the persistent `jam` executable rather than an npx pin?
|
|
69
|
+
*
|
|
70
|
+
* `jam: jam serve`, `jam: /usr/local/bin/jam serve`, `jam: C:\...\jam.cmd serve`
|
|
71
|
+
* all count; an npx line never does - its command token is `npx`. A bare entry
|
|
72
|
+
* carries no version in the listing, so its staleness has to be measured from
|
|
73
|
+
* the executable it would actually run (bareJamVersion), not from the line.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isBareJamEntry(line: string): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The version a bare `jam` registration actually runs, measured by asking the
|
|
78
|
+
* executable itself. `runtime status --json` answers from ~/.jam/config.yaml
|
|
79
|
+
* and the resolved build - the same resolution the registered entry performs.
|
|
80
|
+
*
|
|
81
|
+
* undefined when `jam` is not on PATH or does not answer: a registration that
|
|
82
|
+
* cannot be measured counts as stale, same as an unreadable pin.
|
|
83
|
+
*/
|
|
84
|
+
export declare function bareJamVersion(run?: HostRunner): string | undefined;
|
|
85
|
+
/** Is this measured launcher version the one this release registers? */
|
|
86
|
+
export declare function preferBareRegistration(version: string | undefined): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Does this entry run the launcher this release registers?
|
|
89
|
+
*
|
|
90
|
+
* An npx pin answers from the line itself. A bare `jam` line names no version,
|
|
91
|
+
* so the caller passes what the executable measured (`bareVersion`) - without
|
|
92
|
+
* it, bare stays stale. Anything else - an older pin, an unpinned spec, a line
|
|
93
|
+
* whose command JAM cannot read - counts as stale. That direction is
|
|
94
|
+
* deliberate: `mcp add` rewrites the same entry, so a needless repair costs
|
|
95
|
+
* one command, while a missed one leaves the agent talking to a server nobody
|
|
96
|
+
* tested it against.
|
|
97
|
+
*/
|
|
98
|
+
export declare function isEntryStale(line: string, bareVersion?: string): boolean;
|
|
45
99
|
/**
|
|
46
100
|
* Ask each host what it has, and whether it is there at all.
|
|
47
101
|
*
|
|
@@ -23,6 +23,13 @@ export const defaultHostRunner = ({ command, args }) => {
|
|
|
23
23
|
* Windows shell, and every argument here is a bare token instead.
|
|
24
24
|
*/
|
|
25
25
|
const LAUNCH = ["--", JAM_MCP_ENTRY.command, ...JAM_MCP_ENTRY.args];
|
|
26
|
+
/**
|
|
27
|
+
* What a persistent install registers: the global `jam` executable, no
|
|
28
|
+
* package runner, no cache. Only offered when the measured global launcher
|
|
29
|
+
* is exactly this release (preferBareRegistration) - registering bare over
|
|
30
|
+
* an older global would silently downgrade the served toolset.
|
|
31
|
+
*/
|
|
32
|
+
const LAUNCH_BARE = ["--", "jam", "serve"];
|
|
26
33
|
const ADAPTERS = [
|
|
27
34
|
{
|
|
28
35
|
id: "claude-code",
|
|
@@ -30,15 +37,27 @@ const ADAPTERS = [
|
|
|
30
37
|
// `-s user` is the whole point: registered for this user on this machine,
|
|
31
38
|
// not for whichever project happens to be open.
|
|
32
39
|
register: { command: "claude", args: ["mcp", "add", "jam", "-s", "user", ...LAUNCH] },
|
|
40
|
+
unregister: { command: "claude", args: ["mcp", "remove", "jam", "-s", "user"] },
|
|
33
41
|
},
|
|
34
42
|
{
|
|
35
43
|
id: "codex",
|
|
36
44
|
probe: { command: "codex", args: ["mcp", "list"] },
|
|
37
45
|
register: { command: "codex", args: ["mcp", "add", "jam", ...LAUNCH] },
|
|
46
|
+
unregister: { command: "codex", args: ["mcp", "remove", "jam"] },
|
|
38
47
|
},
|
|
39
48
|
];
|
|
40
|
-
export function hostRegistration(id) {
|
|
41
|
-
|
|
49
|
+
export function hostRegistration(id, options = {}) {
|
|
50
|
+
const adapter = ADAPTERS.find((a) => a.id === id);
|
|
51
|
+
if (!adapter)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!options.bare)
|
|
54
|
+
return adapter.register;
|
|
55
|
+
const at = adapter.register.args.indexOf("--");
|
|
56
|
+
return { command: adapter.register.command, args: [...adapter.register.args.slice(0, at), ...LAUNCH_BARE] };
|
|
57
|
+
}
|
|
58
|
+
/** The removal that has to precede re-registering an entry this host already has. */
|
|
59
|
+
export function hostUnregistration(id) {
|
|
60
|
+
return ADAPTERS.find((a) => a.id === id)?.unregister;
|
|
42
61
|
}
|
|
43
62
|
const ANSI = /\[[0-9;?]*[A-Za-z]/g;
|
|
44
63
|
/**
|
|
@@ -54,11 +73,76 @@ const ANSI = /\[[0-9;?]*[A-Za-z]/g;
|
|
|
54
73
|
* `mcp add` on an existing entry writes the same launcher line back.
|
|
55
74
|
*/
|
|
56
75
|
export function listsJamEntry(stdout) {
|
|
57
|
-
return stdout
|
|
76
|
+
return jamEntryLine(stdout) !== null;
|
|
77
|
+
}
|
|
78
|
+
/** The listing line for `jam`, ANSI stripped, or null when there is none. */
|
|
79
|
+
export function jamEntryLine(stdout) {
|
|
80
|
+
const line = stdout
|
|
58
81
|
.replace(ANSI, "")
|
|
59
82
|
.split(/\r?\n/)
|
|
60
|
-
.
|
|
83
|
+
.find((candidate) => /^\s*jam(?=[\s:])/.test(candidate));
|
|
84
|
+
return line ?? null;
|
|
85
|
+
}
|
|
86
|
+
/** The launcher version a listing line runs, when the line names one. */
|
|
87
|
+
export function entryLauncherVersion(line) {
|
|
88
|
+
return /@jam-mcp\/launcher@([^\s"']+)/.exec(line)?.[1];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Does this entry run the persistent `jam` executable rather than an npx pin?
|
|
92
|
+
*
|
|
93
|
+
* `jam: jam serve`, `jam: /usr/local/bin/jam serve`, `jam: C:\...\jam.cmd serve`
|
|
94
|
+
* all count; an npx line never does - its command token is `npx`. A bare entry
|
|
95
|
+
* carries no version in the listing, so its staleness has to be measured from
|
|
96
|
+
* the executable it would actually run (bareJamVersion), not from the line.
|
|
97
|
+
*/
|
|
98
|
+
export function isBareJamEntry(line) {
|
|
99
|
+
const command = line.replace(/^\s*jam\s*:?\s*/, "");
|
|
100
|
+
return /^(?:\S*[\\/])?jam(?:\.cmd|\.exe)?["']?\s+serve\b/i.test(command);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The version a bare `jam` registration actually runs, measured by asking the
|
|
104
|
+
* executable itself. `runtime status --json` answers from ~/.jam/config.yaml
|
|
105
|
+
* and the resolved build - the same resolution the registered entry performs.
|
|
106
|
+
*
|
|
107
|
+
* undefined when `jam` is not on PATH or does not answer: a registration that
|
|
108
|
+
* cannot be measured counts as stale, same as an unreadable pin.
|
|
109
|
+
*/
|
|
110
|
+
export function bareJamVersion(run = defaultHostRunner) {
|
|
111
|
+
const result = run({ command: "jam", args: ["runtime", "status", "--json"] });
|
|
112
|
+
if (result.failed || result.status !== 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
try {
|
|
115
|
+
const version = JSON.parse(result.stdout).version;
|
|
116
|
+
return typeof version === "string" ? version : undefined;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Is this measured launcher version the one this release registers? */
|
|
123
|
+
export function preferBareRegistration(version) {
|
|
124
|
+
return version !== undefined && version === EXPECTED_LAUNCHER_VERSION;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Does this entry run the launcher this release registers?
|
|
128
|
+
*
|
|
129
|
+
* An npx pin answers from the line itself. A bare `jam` line names no version,
|
|
130
|
+
* so the caller passes what the executable measured (`bareVersion`) - without
|
|
131
|
+
* it, bare stays stale. Anything else - an older pin, an unpinned spec, a line
|
|
132
|
+
* whose command JAM cannot read - counts as stale. That direction is
|
|
133
|
+
* deliberate: `mcp add` rewrites the same entry, so a needless repair costs
|
|
134
|
+
* one command, while a missed one leaves the agent talking to a server nobody
|
|
135
|
+
* tested it against.
|
|
136
|
+
*/
|
|
137
|
+
export function isEntryStale(line, bareVersion) {
|
|
138
|
+
const pinned = entryLauncherVersion(line);
|
|
139
|
+
if (pinned !== undefined)
|
|
140
|
+
return pinned !== EXPECTED_LAUNCHER_VERSION;
|
|
141
|
+
if (isBareJamEntry(line))
|
|
142
|
+
return bareVersion !== EXPECTED_LAUNCHER_VERSION;
|
|
143
|
+
return true;
|
|
61
144
|
}
|
|
145
|
+
const EXPECTED_LAUNCHER_VERSION = entryLauncherVersion(JAM_MCP_ENTRY.args.join(" "));
|
|
62
146
|
/**
|
|
63
147
|
* Ask each host what it has, and whether it is there at all.
|
|
64
148
|
*
|
|
@@ -80,17 +164,32 @@ export function detectHosts(run = defaultHostRunner) {
|
|
|
80
164
|
}
|
|
81
165
|
let cachedHosts;
|
|
82
166
|
function probeHosts(run) {
|
|
167
|
+
let bareCache;
|
|
83
168
|
return ADAPTERS.map((adapter) => {
|
|
84
169
|
const result = run(adapter.probe);
|
|
85
170
|
if (result.failed || result.status !== 0) {
|
|
86
171
|
return { id: adapter.id, cliAvailable: false, hasJamEntry: false };
|
|
87
172
|
}
|
|
173
|
+
const line = jamEntryLine(result.stdout);
|
|
174
|
+
if (!line)
|
|
175
|
+
return { id: adapter.id, cliAvailable: true, hasJamEntry: false };
|
|
176
|
+
// A bare entry's version lives in the executable, not the line - measure
|
|
177
|
+
// it once, only when a bare entry actually shows up.
|
|
178
|
+
const bare = isBareJamEntry(line);
|
|
179
|
+
const version = entryLauncherVersion(line) ?? (bare ? measuredBare() : undefined);
|
|
88
180
|
return {
|
|
89
181
|
id: adapter.id,
|
|
90
182
|
cliAvailable: true,
|
|
91
|
-
hasJamEntry:
|
|
183
|
+
hasJamEntry: true,
|
|
184
|
+
...(version ? { entryVersion: version } : {}),
|
|
185
|
+
...(bare ? { entryBare: true } : {}),
|
|
186
|
+
entryStale: isEntryStale(line, version),
|
|
92
187
|
};
|
|
93
188
|
});
|
|
189
|
+
function measuredBare() {
|
|
190
|
+
bareCache ??= { version: bareJamVersion(run) };
|
|
191
|
+
return bareCache.version;
|
|
192
|
+
}
|
|
94
193
|
}
|
|
95
194
|
/** How a person would do it by hand, for the hosts JAM could not reach. */
|
|
96
195
|
export function describeHostCommand({ command, args }) {
|
|
@@ -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.4.
|
|
16
|
+
readonly args: readonly ["--yes", "@jam-mcp/launcher@1.4.2", "serve"];
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
19
|
* Recognise wiring from before the launcher existed: a hard-coded `node` path
|
|
@@ -36,6 +36,11 @@ export function applySetupPlan(plan, options = {}) {
|
|
|
36
36
|
// decides nothing. A failure is that host's failure, reported with the
|
|
37
37
|
// command that produced it - never retried against another host.
|
|
38
38
|
const run = options.runHost ?? defaultHostRunner;
|
|
39
|
+
// A repair removes the stale entry first. Its failure is not reported on
|
|
40
|
+
// its own: if the entry is already gone the removal fails harmlessly, and
|
|
41
|
+
// if it is still there the registration below fails and says so.
|
|
42
|
+
if (change.precede)
|
|
43
|
+
run({ command: change.precede.command, args: change.precede.args });
|
|
39
44
|
const result = run({ command: change.command, args: change.args });
|
|
40
45
|
if (result.failed || result.status !== 0) {
|
|
41
46
|
throw new Error(`Registering JAM with ${change.host} failed: ${change.command} ${change.args.join(" ")}`);
|
|
@@ -34,7 +34,7 @@ export type SetupChange = {
|
|
|
34
34
|
/** Present on a rebind, so the preview shows what is being replaced. */
|
|
35
35
|
previousKey?: string;
|
|
36
36
|
} | {
|
|
37
|
-
type: "create";
|
|
37
|
+
type: "create" | "replace";
|
|
38
38
|
target: "host-mcp";
|
|
39
39
|
host: HostId;
|
|
40
40
|
/**
|
|
@@ -44,6 +44,18 @@ export type SetupChange = {
|
|
|
44
44
|
*/
|
|
45
45
|
command: string;
|
|
46
46
|
args: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Run before the registration, on a repair. The host CLI refuses to add an
|
|
49
|
+
* entry that already exists, so the stale one has to go first - and which
|
|
50
|
+
* command does that is decided here, not worked out during apply.
|
|
51
|
+
*/
|
|
52
|
+
precede?: {
|
|
53
|
+
command: string;
|
|
54
|
+
args: string[];
|
|
55
|
+
};
|
|
56
|
+
/** On a repair: the launcher pin the entry runs today, so the preview names it. */
|
|
57
|
+
previousVersion?: string;
|
|
58
|
+
reason?: "stale-registration";
|
|
47
59
|
};
|
|
48
60
|
export type SetupPlan = {
|
|
49
61
|
status: SetupStatus;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { CONFIG_RELATIVE_PATH } from "../config/load-config.js";
|
|
3
|
-
import { hostRegistration } from "./host-mcp.js";
|
|
3
|
+
import { hostRegistration, preferBareRegistration, hostUnregistration } from "./host-mcp.js";
|
|
4
4
|
import { projectBindingsPath } from "./project-bindings.js";
|
|
5
5
|
import { decideProjectKey } from "./project-config-bootstrapper.js";
|
|
6
6
|
import { portableBootstrapCommand } from "@jam-mcp/launcher";
|
|
@@ -155,17 +155,29 @@ function finish(changes, state, project) {
|
|
|
155
155
|
function planHostChanges(state) {
|
|
156
156
|
const changes = [];
|
|
157
157
|
for (const host of state.hosts) {
|
|
158
|
-
if (!host.cliAvailable
|
|
158
|
+
if (!host.cliAvailable)
|
|
159
159
|
continue;
|
|
160
|
-
|
|
160
|
+
// An entry that exists but runs an older launcher is not "already set up":
|
|
161
|
+
// that pin decides which server, and so which tools, the agent actually gets.
|
|
162
|
+
if (host.hasJamEntry && host.entryStale !== true)
|
|
163
|
+
continue;
|
|
164
|
+
// A machine whose global `jam` already runs this release gets the
|
|
165
|
+
// persistent registration; anything else keeps the npx pin fallback.
|
|
166
|
+
const registration = hostRegistration(host.id, {
|
|
167
|
+
bare: preferBareRegistration(state.bareLauncher),
|
|
168
|
+
});
|
|
161
169
|
if (!registration)
|
|
162
170
|
continue;
|
|
171
|
+
const removal = hostUnregistration(host.id);
|
|
163
172
|
changes.push({
|
|
164
|
-
type: "create",
|
|
173
|
+
type: host.hasJamEntry ? "replace" : "create",
|
|
165
174
|
target: "host-mcp",
|
|
166
175
|
host: host.id,
|
|
167
176
|
command: registration.command,
|
|
168
177
|
args: registration.args,
|
|
178
|
+
...(host.hasJamEntry && removal ? { precede: { command: removal.command, args: removal.args } } : {}),
|
|
179
|
+
...(host.entryVersion ? { previousVersion: host.entryVersion } : {}),
|
|
180
|
+
...(host.hasJamEntry ? { reason: "stale-registration" } : {}),
|
|
169
181
|
});
|
|
170
182
|
}
|
|
171
183
|
return changes;
|
|
@@ -50,6 +50,12 @@ export type SetupState = {
|
|
|
50
50
|
* host, which `jam doctor` has no reason to pay.
|
|
51
51
|
*/
|
|
52
52
|
hosts: HostState[];
|
|
53
|
+
/**
|
|
54
|
+
* The version the global `jam` executable actually runs, when one answers.
|
|
55
|
+
* Measured with the hosts (same probe budget); undefined otherwise. This is
|
|
56
|
+
* what decides whether a repair registers bare `jam` or an npx pin.
|
|
57
|
+
*/
|
|
58
|
+
bareLauncher?: string;
|
|
53
59
|
};
|
|
54
60
|
export type DetectOptions = {
|
|
55
61
|
cwd?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readRuntimeConfig, resolveRuntime, } from "@jam-mcp/launcher";
|
|
2
2
|
import { CompositeCredentialProvider } from "../adapters/credentials/composite.js";
|
|
3
3
|
import { loadConfig } from "../config/load-config.js";
|
|
4
|
-
import { detectHosts } from "./host-mcp.js";
|
|
4
|
+
import { bareJamVersion, detectHosts } from "./host-mcp.js";
|
|
5
5
|
import { inspectMcpConfig, isLegacyJamEntry } from "./mcp-config-merger.js";
|
|
6
6
|
import { inspectProjectBindings } from "./project-bindings.js";
|
|
7
7
|
import { resolveProjectRoot } from "./project-root-resolver.js";
|
|
@@ -31,6 +31,12 @@ export function detectSetupState(options = {}) {
|
|
|
31
31
|
project: { ...detectProject(located), ...(binding ? { binding } : {}) },
|
|
32
32
|
mcp: detectMcp(located.root),
|
|
33
33
|
hosts: options.probeHosts ? detectHosts(options.runHost) : [],
|
|
34
|
+
...(options.probeHosts
|
|
35
|
+
? (() => {
|
|
36
|
+
const measured = bareJamVersion(options.runHost);
|
|
37
|
+
return measured !== undefined ? { bareLauncher: measured } : {};
|
|
38
|
+
})()
|
|
39
|
+
: {}),
|
|
34
40
|
};
|
|
35
41
|
}
|
|
36
42
|
function detectRuntime(home) {
|
package/dist/cli/agent-api.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type MigrationTarget } from "../bootstrap/migration-target.js";
|
|
2
2
|
import type { CredentialPort } from "../ports/credentials.port.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type HostRunner } from "../bootstrap/host-mcp.js";
|
|
4
|
+
import { type ToolsetProbe } from "../bootstrap/live-toolset.js";
|
|
4
5
|
import type { GitRemoteFn } from "../bootstrap/workspace-identity.js";
|
|
5
6
|
/**
|
|
6
7
|
* The machine-readable half of setup.
|
|
@@ -27,6 +28,8 @@ export type AgentOptions = {
|
|
|
27
28
|
env?: NodeJS.ProcessEnv;
|
|
28
29
|
/** Injected by tests so a plan never shells out to npm to verify a migration target. */
|
|
29
30
|
migrationTarget?: MigrationTarget;
|
|
31
|
+
/** Injected by tests so doctor never launches a real MCP server to read its tools. */
|
|
32
|
+
toolsetProbe?: ToolsetProbe;
|
|
30
33
|
/** Injected by tests so identity never depends on the checkout under test. */
|
|
31
34
|
git?: GitRemoteFn;
|
|
32
35
|
/** Injected by tests so no test ever registers JAM with a real host. */
|
|
@@ -52,7 +55,15 @@ export declare function setupApplyCommand(options?: AgentOptions): Promise<numbe
|
|
|
52
55
|
* authenticating), and reports exactly how far it got.
|
|
53
56
|
*/
|
|
54
57
|
export declare function setupAgentCommand(options?: AgentOptions): Promise<number>;
|
|
55
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* `jam doctor --json`.
|
|
60
|
+
*
|
|
61
|
+
* Three axes, kept apart on purpose. The package on this machine being current
|
|
62
|
+
* says nothing about what the host registration launches, and neither says what
|
|
63
|
+
* the agent can actually call - a stale pin serves an older tool set while every
|
|
64
|
+
* local check passes. Reporting them as one verdict is how a broken setup was
|
|
65
|
+
* reported as ready.
|
|
66
|
+
*/
|
|
56
67
|
export declare function doctorJsonCommand(options?: AgentOptions): Promise<number>;
|
|
57
68
|
/**
|
|
58
69
|
* `jam auth status --json` - presence and origin only.
|
package/dist/cli/agent-api.js
CHANGED
|
@@ -5,6 +5,9 @@ import { applySetupPlan } from "../bootstrap/setup-apply.js";
|
|
|
5
5
|
import { detectSetupState } from "../bootstrap/setup-state.js";
|
|
6
6
|
import { buildDeps } from "../deps.js";
|
|
7
7
|
import { toJamError } from "../domain/errors.js";
|
|
8
|
+
import { hostRegistration } from "../bootstrap/host-mcp.js";
|
|
9
|
+
import { checkLiveToolset } from "../bootstrap/live-toolset.js";
|
|
10
|
+
import { SERVER_VERSION } from "@jam-mcp/launcher";
|
|
8
11
|
export function emitJson(payload) {
|
|
9
12
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
10
13
|
}
|
|
@@ -108,17 +111,77 @@ export async function setupAgentCommand(options = {}) {
|
|
|
108
111
|
});
|
|
109
112
|
return health.passed ? 0 : 1;
|
|
110
113
|
}
|
|
111
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* `jam doctor --json`.
|
|
116
|
+
*
|
|
117
|
+
* Three axes, kept apart on purpose. The package on this machine being current
|
|
118
|
+
* says nothing about what the host registration launches, and neither says what
|
|
119
|
+
* the agent can actually call - a stale pin serves an older tool set while every
|
|
120
|
+
* local check passes. Reporting them as one verdict is how a broken setup was
|
|
121
|
+
* reported as ready.
|
|
122
|
+
*/
|
|
112
123
|
export async function doctorJsonCommand(options = {}) {
|
|
124
|
+
// detect() already probes the hosts for every non-shared path; doctor wants that.
|
|
113
125
|
const state = detect(options);
|
|
114
126
|
const health = await gateResult(state.project.root);
|
|
127
|
+
const axes = await inspectAxes(state, options);
|
|
128
|
+
// 등록이 아예 없는 것은 결함이 아니다 — 새 머신, 호스트 CLI 없는 CI, 아직 setup 을
|
|
129
|
+
// 안 한 사용자 모두 정상 상태다. 전체 판정을 무너뜨리는 것은 **거짓말하는 상태**뿐:
|
|
130
|
+
// 낡은 핀을 실행 중인 등록(STALE)과, 등록은 맞는데 실제 도구가 다른 경우(MISMATCH).
|
|
131
|
+
const axesOk = axes.registration !== "HOST_REGISTRATION_STALE" && axes.live !== "LIVE_TOOLSET_MISMATCH";
|
|
132
|
+
const passed = health.passed && axesOk;
|
|
115
133
|
emitJson({
|
|
116
|
-
status:
|
|
134
|
+
status: passed ? "ready" : "failed",
|
|
117
135
|
...(health.error ? { error: health.error } : {}),
|
|
118
136
|
project: { root: state.project.root, ...(state.project.key ? { key: state.project.key } : {}) },
|
|
137
|
+
axes,
|
|
119
138
|
checks: health.checks,
|
|
120
139
|
});
|
|
121
|
-
return
|
|
140
|
+
return passed ? 0 : 1;
|
|
141
|
+
}
|
|
142
|
+
async function inspectAxes(state, options) {
|
|
143
|
+
const packageVersion = state.runtime.version;
|
|
144
|
+
const axes = {
|
|
145
|
+
package: packageVersion === SERVER_VERSION ? "PACKAGE_READY" : "PACKAGE_NOT_READY",
|
|
146
|
+
...(packageVersion ? { packageVersion } : {}),
|
|
147
|
+
registration: "UNREGISTERED",
|
|
148
|
+
live: "UNCHECKED",
|
|
149
|
+
};
|
|
150
|
+
const hosts = state.hosts.filter((host) => host.cliAvailable);
|
|
151
|
+
if (state.hosts.length > 0 && hosts.length === 0) {
|
|
152
|
+
return { ...axes, registration: "HOST_UNREACHABLE", detail: "no host CLI answered" };
|
|
153
|
+
}
|
|
154
|
+
const registered = hosts.find((host) => host.hasJamEntry);
|
|
155
|
+
if (!registered)
|
|
156
|
+
return axes;
|
|
157
|
+
axes.registration = registered.entryStale ? "HOST_REGISTRATION_STALE" : "OK";
|
|
158
|
+
if (registered.entryVersion)
|
|
159
|
+
axes.registeredVersion = registered.entryVersion;
|
|
160
|
+
// A stale entry has already answered the question the live check would ask,
|
|
161
|
+
// and asking it means launching that older release. Repair first.
|
|
162
|
+
if (registered.entryStale)
|
|
163
|
+
return axes;
|
|
164
|
+
// Launch what is actually registered: a bare entry runs the global `jam`,
|
|
165
|
+
// an npx pin runs the pinned launcher. Testing the other one would prove
|
|
166
|
+
// nothing about the entry the agent uses.
|
|
167
|
+
const registration = hostRegistration(registered.id, { bare: registered.entryBare === true });
|
|
168
|
+
const launch = registration ? launcherArgv(registration.args) : null;
|
|
169
|
+
if (!launch)
|
|
170
|
+
return { ...axes, live: "UNCHECKED", detail: "could not read the registered command" };
|
|
171
|
+
const result = await checkLiveToolset(launch, options.toolsetProbe);
|
|
172
|
+
axes.live = result.verdict === "OK" ? "OK" : result.verdict;
|
|
173
|
+
if (result.missing && result.missing.length > 0)
|
|
174
|
+
axes.missingTools = result.missing;
|
|
175
|
+
if (result.detail)
|
|
176
|
+
axes.detail = result.detail;
|
|
177
|
+
return axes;
|
|
178
|
+
}
|
|
179
|
+
/** The registration argv carries the launch command after `--`. */
|
|
180
|
+
function launcherArgv(args) {
|
|
181
|
+
const at = args.indexOf("--");
|
|
182
|
+
if (at < 0 || args.length <= at + 1)
|
|
183
|
+
return null;
|
|
184
|
+
return { command: args[at + 1], args: args.slice(at + 2) };
|
|
122
185
|
}
|
|
123
186
|
/**
|
|
124
187
|
* `jam auth status --json` - presence and origin only.
|
package/dist/cli/setup.js
CHANGED
|
@@ -108,7 +108,9 @@ function reportApplied(applied, plan) {
|
|
|
108
108
|
continue;
|
|
109
109
|
}
|
|
110
110
|
if (change.target === "host-mcp") {
|
|
111
|
-
line(
|
|
111
|
+
line(change.type === "replace"
|
|
112
|
+
? `[OK] ${change.host} - jam re-registered, replacing a stale launcher pin${change.previousVersion ? ` (${change.previousVersion})` : ""}`
|
|
113
|
+
: `[OK] ${change.host} - jam registered for this user`);
|
|
112
114
|
continue;
|
|
113
115
|
}
|
|
114
116
|
if (change.target === "project-config") {
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -2,6 +2,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import type { JamDeps } from "../deps.js";
|
|
3
3
|
export declare const SERVER_NAME = "jam";
|
|
4
4
|
export declare const TOOL_COUNT: number;
|
|
5
|
+
/**
|
|
6
|
+
* The same contract by name, for the checks that ask a *running* server what it
|
|
7
|
+
* serves. The registrars above cannot be introspected for their tool names, and
|
|
8
|
+
* counting is not enough: five tools with one renamed is still five.
|
|
9
|
+
* `tests/contract/tools.test.ts` holds this list to what the server registers.
|
|
10
|
+
*/
|
|
11
|
+
export declare const TOOL_NAMES: readonly ["jira_search", "jira_context", "jira_full", "jira_write_plan", "jira_write_apply"];
|
|
5
12
|
/**
|
|
6
13
|
* The external contract: three read tools and two write tools.
|
|
7
14
|
*
|
|
@@ -21,6 +21,19 @@ const REGISTER_TOOLS = [
|
|
|
21
21
|
registerJiraWriteApply,
|
|
22
22
|
];
|
|
23
23
|
export const TOOL_COUNT = REGISTER_TOOLS.length;
|
|
24
|
+
/**
|
|
25
|
+
* The same contract by name, for the checks that ask a *running* server what it
|
|
26
|
+
* serves. The registrars above cannot be introspected for their tool names, and
|
|
27
|
+
* counting is not enough: five tools with one renamed is still five.
|
|
28
|
+
* `tests/contract/tools.test.ts` holds this list to what the server registers.
|
|
29
|
+
*/
|
|
30
|
+
export const TOOL_NAMES = [
|
|
31
|
+
"jira_search",
|
|
32
|
+
"jira_context",
|
|
33
|
+
"jira_full",
|
|
34
|
+
"jira_write_plan",
|
|
35
|
+
"jira_write_apply",
|
|
36
|
+
];
|
|
24
37
|
/**
|
|
25
38
|
* The external contract: three read tools and two write tools.
|
|
26
39
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jam-mcp/server",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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.4.
|
|
44
|
+
"@jam-mcp/launcher": "1.4.2",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
46
|
"yaml": "^2.9.0",
|
|
47
47
|
"zod": "^4.4.3"
|