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:
parent
79b80b459d
commit
7815548300
8 changed files with 178 additions and 66 deletions
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-cli",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "CLI for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { dirname, join } from "path";
|
||||
import { fileURLToPath, pathToFileURL } from "url";
|
||||
import { Command } from "commander";
|
||||
type McpResponse = Record<string, unknown>;
|
||||
import { awaitableLog } from "./utils/awaitable-log.js";
|
||||
|
|
@ -49,11 +47,15 @@ import {
|
|||
LoggingLevelSchema,
|
||||
type LoggingLevel,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { readInspectorVersion } from "@inspector/core/node/version.js";
|
||||
|
||||
export const validLogLevels: LoggingLevel[] = Object.values(
|
||||
LoggingLevelSchema.enum,
|
||||
);
|
||||
|
||||
/** Client identity name the CLI reports to servers. */
|
||||
const CLI_CLIENT_NAME = "inspector-cli";
|
||||
|
||||
type MethodArgs = {
|
||||
method?: string;
|
||||
promptName?: string;
|
||||
|
|
@ -74,20 +76,13 @@ async function callMethod(
|
|||
cliAuthOverrides: RunnerClientConfigOverrides,
|
||||
callbackUrlConfig: RunnerOAuthCallbackConfig,
|
||||
): Promise<void> {
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const packageJsonPath = join(__dirname, "../package.json");
|
||||
const packageJsonData = await import(pathToFileURL(packageJsonPath).href, {
|
||||
with: { type: "json" },
|
||||
});
|
||||
const packageJson = packageJsonData.default as {
|
||||
name: string;
|
||||
version: string;
|
||||
// Version comes from the single source of truth — the root package.json —
|
||||
// via the shared core reader, not the CLI's own manifest.
|
||||
const clientIdentity = {
|
||||
name: CLI_CLIENT_NAME,
|
||||
version: readInspectorVersion(import.meta.url),
|
||||
};
|
||||
|
||||
const [, name = packageJson.name] = packageJson.name.split("/");
|
||||
const version = packageJson.version;
|
||||
const clientIdentity = { name, version };
|
||||
|
||||
const environment: InspectorClientEnvironment = {
|
||||
transport: createTransportNode,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-launcher",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "Launcher for MCP Inspector (web, CLI, TUI)",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-tui",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"description": "Terminal User Interface (TUI) for the Model Context Protocol inspector",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -6,9 +6,6 @@ import React, {
|
|||
useRef,
|
||||
} from "react";
|
||||
import { Box, Text, useInput, useApp, type Key } from "ink";
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import type {
|
||||
MessageEntry,
|
||||
FetchRequestEntry,
|
||||
|
|
@ -56,6 +53,7 @@ import {
|
|||
isOAuthCapableServerConfig,
|
||||
} from "@inspector/core/client/runner.js";
|
||||
import { formatRunnerOAuthRedirectUrl } from "@inspector/core/auth/node/runner-oauth-callback.js";
|
||||
import { readInspectorVersion } from "@inspector/core/node/version.js";
|
||||
import {
|
||||
createOAuthCallbackServer,
|
||||
type OAuthCallbackServer,
|
||||
|
|
@ -85,33 +83,17 @@ import { PromptTestModal } from "./components/PromptTestModal.js";
|
|||
import { DetailsModal } from "./components/DetailsModal.js";
|
||||
import type { TuiServer } from "./tui-servers.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
// Header branding. The version is the single source of truth — the root
|
||||
// package.json — read via the shared core reader; the name/description are the
|
||||
// TUI's own display strings (they live in code, not the npm manifest, which no
|
||||
// longer carries them per-client).
|
||||
const APP_NAME = "MCP Inspector TUI";
|
||||
const APP_DESCRIPTION =
|
||||
"Terminal User Interface for the Model Context Protocol Inspector";
|
||||
const APP_VERSION = readInspectorVersion(import.meta.url);
|
||||
|
||||
// Read package.json to get project info
|
||||
// Strategy: Try multiple paths to handle both local dev and global install
|
||||
// - Local dev (tsx): __dirname = src/, package.json is one level up
|
||||
// - Global install: __dirname = dist/src/, package.json is two levels up
|
||||
let packagePath: string;
|
||||
let packageJson: { name: string; description: string; version: string };
|
||||
|
||||
try {
|
||||
// Try two levels up first (global install case)
|
||||
packagePath = join(__dirname, "..", "..", "package.json");
|
||||
packageJson = JSON.parse(readFileSync(packagePath, "utf-8")) as {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
};
|
||||
} catch {
|
||||
// Fall back to one level up (local dev case)
|
||||
packagePath = join(__dirname, "..", "package.json");
|
||||
packageJson = JSON.parse(readFileSync(packagePath, "utf-8")) as {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
/** Client identity name the TUI reports to servers. */
|
||||
const TUI_CLIENT_NAME = "inspector-tui";
|
||||
|
||||
// Focus management types
|
||||
type FocusArea =
|
||||
|
|
@ -310,6 +292,7 @@ function App({
|
|||
);
|
||||
const opts: InspectorClientOptions = {
|
||||
environment,
|
||||
clientIdentity: { name: TUI_CLIENT_NAME, version: APP_VERSION },
|
||||
pipeStderr: true,
|
||||
...(savedSettings &&
|
||||
savedSettings.requestTimeout > 0 && {
|
||||
|
|
@ -1498,11 +1481,11 @@ function App({
|
|||
>
|
||||
<Box>
|
||||
<Text bold color="cyan">
|
||||
{packageJson.name}
|
||||
{APP_NAME}
|
||||
</Text>
|
||||
<Text dimColor> - {packageJson.description}</Text>
|
||||
<Text dimColor> - {APP_DESCRIPTION}</Text>
|
||||
</Box>
|
||||
<Text dimColor>v{packageJson.version}</Text>
|
||||
<Text dimColor>v{APP_VERSION}</Text>
|
||||
</Box>
|
||||
|
||||
{/* Main content area */}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/inspector-web",
|
||||
"private": true,
|
||||
"version": "2.0.0",
|
||||
"type": "module",
|
||||
"main": "build/index.js",
|
||||
"bin": {
|
||||
|
|
|
|||
69
clients/web/src/test/integration/node/version.test.ts
Normal file
69
clients/web/src/test/integration/node/version.test.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -109,6 +109,7 @@ export default defineConfig(({ command }) => {
|
|||
path.join(repoRoot, 'core/auth/**/*.{ts,tsx}'),
|
||||
path.join(repoRoot, 'core/storage/**/*.{ts,tsx}'),
|
||||
path.join(repoRoot, 'core/logging/**/*.{ts,tsx}'),
|
||||
path.join(repoRoot, 'core/node/**/*.{ts,tsx}'),
|
||||
],
|
||||
exclude: [
|
||||
'**/*.stories.{ts,tsx}',
|
||||
|
|
|
|||
Loading…
Reference in a new issue