refactor(version): single-source the version from the root package.json

The Inspector ships as one package with one version, but four client
package.jsons each carried their own `version` and were read at runtime
(cli.ts, tui App.tsx), and core hardcoded a third, stale value (0.20.0) that
the TUI and web actually reported to servers. Consolidate onto the root
package.json as the single source of truth.

- Add core/node/version.ts: shared, Node-only readInspectorVersion() that walks
  up to the root package.json (always shipped in the tarball). 100% covered by a
  new integration test.
- CLI and TUI read the version via the shared reader instead of their own
  manifest; TUI now also passes a real clientIdentity (was falling through to
  the stale core default).
- Drop the `version` field from all four client package.jsons; the root keeps
  the only version. TUI header name/description become local display constants.
- core default client identity: replace the stale 0.20.0 hardcode with a neutral
  0.0.0 placeholder (real clients pass clientIdentity; web will source its
  version from GET /api/config, see #1639).
- Stop shipping clients/cli/package.json and clients/tui/package.json (no longer
  read at runtime) — supersedes the earlier "ship them" fix; update pack:verify
  and the root README packaging invariants accordingly.
- Add core/node/** to the web coverage include so the new module is gated.
- Add the shared-code architecture diagram to the root README.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw
This commit is contained in:
cliffhall 2026-07-10 17:31:21 -04:00
commit 7815548300

View file

@ -0,0 +1,69 @@
import { describe, it, expect } from "vitest";
import {
mkdtempSync,
mkdirSync,
rmSync,
writeFileSync,
readFileSync,
} from "fs";
import { tmpdir } from "os";
import { join, dirname } from "path";
import { pathToFileURL, fileURLToPath } from "url";
import {
readInspectorVersion,
ROOT_PACKAGE_NAME,
} from "@inspector/core/node/version.js";
/** Absolute path to the repo root package.json (three levels above core/node). */
const rootPackageJsonPath = join(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
"..",
"..",
"..",
"..",
"package.json",
);
function readRootVersion(): string {
return (
JSON.parse(readFileSync(rootPackageJsonPath, "utf-8")) as {
version: string;
}
).version;
}
describe("readInspectorVersion", () => {
it("resolves the version from the root package.json for a caller inside the repo", () => {
// This test file lives below the root manifest; walking up from it passes
// clients/web/package.json (wrong name → skipped) and several levels with no
// package.json (the catch branch) before reaching the root.
expect(readInspectorVersion(import.meta.url)).toBe(readRootVersion());
// Sanity: the root manifest really is the one we match on.
expect(ROOT_PACKAGE_NAME).toBe("@modelcontextprotocol/inspector");
});
it("skips a matching-name manifest that has no version, then throws when no versioned root exists above the caller", () => {
// A fake root manifest with the right name but NO version must not satisfy
// the search — the walk keeps going up. Rooted in the OS temp dir (outside
// the repo), so the walk reaches the filesystem root without finding a
// versioned inspector manifest and throws.
const work = mkdtempSync(join(tmpdir(), "version-test-"));
try {
writeFileSync(
join(work, "package.json"),
JSON.stringify({ name: ROOT_PACKAGE_NAME }),
);
const sub = join(work, "sub");
mkdirSync(sub);
const callerUrl = pathToFileURL(join(sub, "caller.js")).href;
expect(() => readInspectorVersion(callerUrl)).toThrow(
/Could not locate the @modelcontextprotocol\/inspector root package\.json/,
);
} finally {
rmSync(work, { recursive: true, force: true });
}
});
});