feat: port CLI, TUI, and launcher from v1.5 into v2 (#1452)
Ports the CLI, TUI, and launcher from v1.5 into v2 behind a single `mcp-inspector` entrypoint (launcher → web/cli/tui). Includes the `mcp-inspector --web --dev` launcher fix (Vite alias resolution + prod-server double-start), the root `postinstall` install cascade with simplified CI, and spec/AGENTS docs (no-workspaces rationale, Development setup). Closes #1246. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
53dc4549be
commit
2488ca5e38
2 changed files with 25594 additions and 236 deletions
27
clients/web/src/test/core/logging/logger.test.ts
Normal file
27
clients/web/src/test/core/logging/logger.test.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { silentLogger } from "../../../../../../core/logging/logger.js";
|
||||
|
||||
describe("silentLogger", () => {
|
||||
it("reports silent level", () => {
|
||||
expect(silentLogger.level).toBe("silent");
|
||||
});
|
||||
|
||||
it("accepts log calls at every level without throwing", () => {
|
||||
expect(() => {
|
||||
silentLogger.fatal("fatal");
|
||||
silentLogger.error("error");
|
||||
silentLogger.warn("warn");
|
||||
silentLogger.info("info");
|
||||
silentLogger.debug("debug");
|
||||
silentLogger.trace("trace");
|
||||
silentLogger.silent("silent");
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("child returns a logger that remains silent", () => {
|
||||
const child = silentLogger.child({ component: "test" });
|
||||
expect(child.level).toBe("silent");
|
||||
expect(() => child.info("nested")).not.toThrow();
|
||||
expect(child.child()).toBe(child);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
parseKeyValuePair,
|
||||
parseHeaderPair,
|
||||
withDefaultConfigPath,
|
||||
resolveServerConfigs,
|
||||
resolveLaunchServerConfigs,
|
||||
getNamedServerConfigs,
|
||||
} from "@inspector/core/mcp/node/config.js";
|
||||
|
||||
|
|
@ -57,6 +59,86 @@ describe("parseHeaderPair", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("withDefaultConfigPath", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "inspector-config-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("leaves options unchanged when --config or ad-hoc target is present", () => {
|
||||
expect(withDefaultConfigPath({ configPath: "/tmp/mcp.json" })).toEqual({
|
||||
configPath: "/tmp/mcp.json",
|
||||
});
|
||||
expect(withDefaultConfigPath({ target: ["node", "server.js"] })).toEqual({
|
||||
target: ["node", "server.js"],
|
||||
});
|
||||
});
|
||||
|
||||
it("injects the default catalog path when no config or ad-hoc target is given", () => {
|
||||
const prevHome = process.env.HOME;
|
||||
const homeDir = join(tempDir, "home");
|
||||
mkdirSync(homeDir, { recursive: true });
|
||||
process.env.HOME = homeDir;
|
||||
try {
|
||||
const options = withDefaultConfigPath({});
|
||||
expect(options.configPath).toBe(
|
||||
join(homeDir, ".mcp-inspector", "mcp.json"),
|
||||
);
|
||||
} finally {
|
||||
process.env.HOME = prevHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveLaunchServerConfigs", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = mkdtempSync(join(tmpdir(), "inspector-config-test-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("uses the default catalog when no target or --config is given", () => {
|
||||
const prevHome = process.env.HOME;
|
||||
process.env.HOME = tempDir;
|
||||
try {
|
||||
expect(() =>
|
||||
resolveLaunchServerConfigs({ target: [] }, "single"),
|
||||
).toThrow(/Config file not found/);
|
||||
} finally {
|
||||
process.env.HOME = prevHome;
|
||||
}
|
||||
});
|
||||
|
||||
it("loads all servers from the default catalog in multi mode", () => {
|
||||
const prevHome = process.env.HOME;
|
||||
const homeDir = join(tempDir, "home");
|
||||
mkdirSync(homeDir, { recursive: true });
|
||||
const defaultConfig = join(homeDir, ".mcp-inspector", "mcp.json");
|
||||
mkdirSync(join(homeDir, ".mcp-inspector"), { recursive: true });
|
||||
writeFileSync(
|
||||
defaultConfig,
|
||||
JSON.stringify({ mcpServers: { a: { command: "a" } } }),
|
||||
);
|
||||
process.env.HOME = homeDir;
|
||||
try {
|
||||
const configs = resolveLaunchServerConfigs({}, "multi");
|
||||
expect(configs).toHaveLength(1);
|
||||
expect(configs[0]).toMatchObject({ type: "stdio", command: "a" });
|
||||
} finally {
|
||||
process.env.HOME = prevHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveServerConfigs — single mode", () => {
|
||||
let tempDir: string;
|
||||
let configPath: string;
|
||||
|
|
@ -170,7 +252,7 @@ describe("resolveServerConfigs — single mode", () => {
|
|||
).toThrow(/Only stdio transport can be used with local commands/);
|
||||
});
|
||||
|
||||
it("rejects an empty target with no URL", () => {
|
||||
it("requires a target when no config path is given", () => {
|
||||
expect(() => resolveServerConfigs({ target: [] }, "single")).toThrow(
|
||||
/Target is required/,
|
||||
);
|
||||
|
|
@ -358,6 +440,12 @@ describe("resolveServerConfigs — multi mode", () => {
|
|||
expect(configs[0]?.type).toBe("stdio");
|
||||
});
|
||||
|
||||
it("requires a target when no config path is given", () => {
|
||||
expect(() => resolveServerConfigs({}, "multi")).toThrow(
|
||||
/Target is required/,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects ad-hoc flags alongside a config path", () => {
|
||||
writeFileSync(
|
||||
configPath,
|
||||
|
|
|
|||
Loading…
Reference in a new issue