@jam-mcp/server 1.4.1 → 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 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.1 doctor`, or just `jam
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.1 init` instead.
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
@@ -30,6 +30,8 @@ export type HostState = {
30
30
  entryVersion?: string;
31
31
  /** The entry is present but does not run the launcher this release registers. */
32
32
  entryStale?: boolean;
33
+ /** The entry runs the global `jam` executable rather than an npx pin. */
34
+ entryBare?: boolean;
33
35
  };
34
36
  export type HostRunResult = {
35
37
  status: number | null;
@@ -40,7 +42,9 @@ export type HostRunResult = {
40
42
  /** Injected by tests. Nothing in this module may reach a real CLI unasked. */
41
43
  export type HostRunner = (command: HostCommand) => HostRunResult;
42
44
  export declare const defaultHostRunner: HostRunner;
43
- export declare function hostRegistration(id: HostId): HostCommand | undefined;
45
+ export declare function hostRegistration(id: HostId, options?: {
46
+ bare?: boolean;
47
+ }): HostCommand | undefined;
44
48
  /** The removal that has to precede re-registering an entry this host already has. */
45
49
  export declare function hostUnregistration(id: HostId): HostCommand | undefined;
46
50
  /**
@@ -60,15 +64,38 @@ export declare function listsJamEntry(stdout: string): boolean;
60
64
  export declare function jamEntryLine(stdout: string): string | null;
61
65
  /** The launcher version a listing line runs, when the line names one. */
62
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;
63
87
  /**
64
88
  * Does this entry run the launcher this release registers?
65
89
  *
66
- * Anything else - an older pin, an unpinned spec, a line whose command JAM
67
- * cannot read - counts as stale. That direction is deliberate: `mcp add`
68
- * rewrites the same entry, so a needless repair costs one command, while a
69
- * missed one leaves the agent talking to a server nobody tested it against.
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.
70
97
  */
71
- export declare function isEntryStale(line: string): boolean;
98
+ export declare function isEntryStale(line: string, bareVersion?: string): boolean;
72
99
  /**
73
100
  * Ask each host what it has, and whether it is there at all.
74
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",
@@ -39,8 +46,14 @@ const ADAPTERS = [
39
46
  unregister: { command: "codex", args: ["mcp", "remove", "jam"] },
40
47
  },
41
48
  ];
42
- export function hostRegistration(id) {
43
- return ADAPTERS.find((a) => a.id === id)?.register;
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] };
44
57
  }
45
58
  /** The removal that has to precede re-registering an entry this host already has. */
46
59
  export function hostUnregistration(id) {
@@ -74,16 +87,60 @@ export function jamEntryLine(stdout) {
74
87
  export function entryLauncherVersion(line) {
75
88
  return /@jam-mcp\/launcher@([^\s"']+)/.exec(line)?.[1];
76
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
+ }
77
126
  /**
78
127
  * Does this entry run the launcher this release registers?
79
128
  *
80
- * Anything else - an older pin, an unpinned spec, a line whose command JAM
81
- * cannot read - counts as stale. That direction is deliberate: `mcp add`
82
- * rewrites the same entry, so a needless repair costs one command, while a
83
- * missed one leaves the agent talking to a server nobody tested it against.
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.
84
136
  */
85
- export function isEntryStale(line) {
86
- return entryLauncherVersion(line) !== EXPECTED_LAUNCHER_VERSION;
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;
87
144
  }
88
145
  const EXPECTED_LAUNCHER_VERSION = entryLauncherVersion(JAM_MCP_ENTRY.args.join(" "));
89
146
  /**
@@ -107,6 +164,7 @@ export function detectHosts(run = defaultHostRunner) {
107
164
  }
108
165
  let cachedHosts;
109
166
  function probeHosts(run) {
167
+ let bareCache;
110
168
  return ADAPTERS.map((adapter) => {
111
169
  const result = run(adapter.probe);
112
170
  if (result.failed || result.status !== 0) {
@@ -115,15 +173,23 @@ function probeHosts(run) {
115
173
  const line = jamEntryLine(result.stdout);
116
174
  if (!line)
117
175
  return { id: adapter.id, cliAvailable: true, hasJamEntry: false };
118
- const version = entryLauncherVersion(line);
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);
119
180
  return {
120
181
  id: adapter.id,
121
182
  cliAvailable: true,
122
183
  hasJamEntry: true,
123
184
  ...(version ? { entryVersion: version } : {}),
124
- entryStale: isEntryStale(line),
185
+ ...(bare ? { entryBare: true } : {}),
186
+ entryStale: isEntryStale(line, version),
125
187
  };
126
188
  });
189
+ function measuredBare() {
190
+ bareCache ??= { version: bareJamVersion(run) };
191
+ return bareCache.version;
192
+ }
127
193
  }
128
194
  /** How a person would do it by hand, for the hosts JAM could not reach. */
129
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.1", "serve"];
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
@@ -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, hostUnregistration } 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";
@@ -161,7 +161,11 @@ function planHostChanges(state) {
161
161
  // that pin decides which server, and so which tools, the agent actually gets.
162
162
  if (host.hasJamEntry && host.entryStale !== true)
163
163
  continue;
164
- const registration = hostRegistration(host.id);
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
+ });
165
169
  if (!registration)
166
170
  continue;
167
171
  const removal = hostUnregistration(host.id);
@@ -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) {
@@ -161,7 +161,10 @@ async function inspectAxes(state, options) {
161
161
  // and asking it means launching that older release. Repair first.
162
162
  if (registered.entryStale)
163
163
  return axes;
164
- const registration = hostRegistration(registered.id);
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 });
165
168
  const launch = registration ? launcherArgv(registration.args) : null;
166
169
  if (!launch)
167
170
  return { ...axes, live: "UNCHECKED", detail: "could not read the registered command" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jam-mcp/server",
3
- "version": "1.4.1",
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.1",
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"