import { afterAll, beforeAll, describe, it, expect, vi } from "vitest"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { runCli } from "./helpers/cli-runner.js"; import { runCli as runCliDirect } from "../src/cli.js"; import { expectCliSuccess, expectCliFailure, expectValidJson, } from "./helpers/assertions.js"; import { NO_SERVER_SENTINEL, createSampleTestConfig, createTestConfig, createInvalidConfig, deleteConfigFile, } from "./helpers/fixtures.js"; import { getTestMcpServerCommand, createTestServerHttp, createEchoTool, createListRootsTool, createTestServerInfo, } from "@modelcontextprotocol/inspector-test-server"; import type { MCPServerConfig } from "@inspector/core/mcp/index.js"; import { createServer, request } from "node:http"; describe("CLI Tests", () => { /** * Proxy variables are cleared for the WHOLE file, not just the proxy tests. * * `createProxyFetch`'s `EnvHttpProxyAgent` is memoized process-wide and captures * its proxy URLs when it is constructed (only `NO_PROXY` is re-read per * request). So on a machine with an ambient `HTTP_PROXY`, any earlier test that * makes a real connection would build that agent against the ambient proxy, and * a later test setting `HTTP_PROXY` to its own server would be talking to an * agent that is not listening. Clearing up front means nothing can build the * singleton before the proxy tests do. */ const AMBIENT_PROXY_VARS = [ "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "NO_PROXY", "no_proxy", ] as const; const ambientProxyEnv: Record = {}; beforeAll(() => { for (const name of AMBIENT_PROXY_VARS) { ambientProxyEnv[name] = process.env[name]; delete process.env[name]; } }); afterAll(() => { for (const name of AMBIENT_PROXY_VARS) { const previous = ambientProxyEnv[name]; if (previous === undefined) delete process.env[name]; else process.env[name] = previous; } }); describe("Basic CLI Mode", () => { it("should execute tools/list successfully", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("tools"); expect(Array.isArray(json.tools)).toBe(true); // Validate expected tools from test-mcp-server const toolNames = json.tools.map((tool: { name: string }) => tool.name); expect(toolNames).toContain("echo"); expect(toolNames).toContain("get_sum"); expect(toolNames).toContain("get_annotated_message"); }); it("should fail with nonexistent method", async () => { const result = await runCli([ NO_SERVER_SENTINEL, "--cli", "--method", "nonexistent/method", ]); expectCliFailure(result); }); it("should fail without method", async () => { const result = await runCli([NO_SERVER_SENTINEL, "--cli"]); expectCliFailure(result); }); }); describe("Environment Variables", () => { it("should accept environment variables", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "-e", "KEY1=value1", "-e", "KEY2=value2", "--cli", "--method", "resources/read", "--uri", "test://env", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("contents"); expect(Array.isArray(json.contents)).toBe(true); expect(json.contents.length).toBeGreaterThan(0); // Parse the env vars from the resource const envVars = JSON.parse(json.contents[0].text); expect(envVars.KEY1).toBe("value1"); expect(envVars.KEY2).toBe("value2"); }); it("should reject invalid environment variable format", async () => { const result = await runCli([ NO_SERVER_SENTINEL, "-e", "INVALID_FORMAT", "--cli", "--method", "tools/list", ]); expectCliFailure(result); }); it("should handle environment variable with equals sign in value", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "-e", "API_KEY=abc123=xyz789==", "--cli", "--method", "resources/read", "--uri", "test://env", ]); expectCliSuccess(result); const json = expectValidJson(result); const envVars = JSON.parse(json.contents[0].text); expect(envVars.API_KEY).toBe("abc123=xyz789=="); }); it("should handle environment variable with base64-encoded value", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "-e", "JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=", "--cli", "--method", "resources/read", "--uri", "test://env", ]); expectCliSuccess(result); const json = expectValidJson(result); const envVars = JSON.parse(json.contents[0].text); expect(envVars.JWT_TOKEN).toBe( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0=", ); }); }); describe("Config File", () => { it("should use config file with CLI mode", async () => { const configPath = createSampleTestConfig(); try { const result = await runCli([ "--config", configPath, "--server", "test-stdio", "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("tools"); expect(Array.isArray(json.tools)).toBe(true); expect(json.tools.length).toBeGreaterThan(0); } finally { deleteConfigFile(configPath); } }); it("should fail when using config file without server name", async () => { const configPath = createSampleTestConfig(); try { const result = await runCli([ "--config", configPath, "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { deleteConfigFile(configPath); } }); it("should fail when using server name without config file", async () => { const result = await runCli([ "--server", "test-stdio", "--cli", "--method", "tools/list", ]); expectCliFailure(result); }); it("should fail with nonexistent config file", async () => { const result = await runCli([ "--config", "./nonexistent-config.json", "--server", "test-stdio", "--cli", "--method", "tools/list", ]); expectCliFailure(result); }); it("should fail with invalid config file format", async () => { // Create invalid config temporarily const invalidConfigPath = createInvalidConfig(); try { const result = await runCli([ "--config", invalidConfigPath, "--server", "test-stdio", "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { deleteConfigFile(invalidConfigPath); } }); it("should fail with nonexistent server in config", async () => { const configPath = createSampleTestConfig(); try { const result = await runCli([ "--config", configPath, "--server", "nonexistent", "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { deleteConfigFile(configPath); } }); }); describe("Catalog File", () => { it("should use a writable catalog with CLI mode", async () => { const catalogPath = createSampleTestConfig(); try { const result = await runCli([ "--catalog", catalogPath, "--server", "test-stdio", "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("tools"); expect(Array.isArray(json.tools)).toBe(true); } finally { deleteConfigFile(catalogPath); } }); it("should fail when --catalog and --config are combined", async () => { const catalogPath = createSampleTestConfig(); try { const result = await runCli([ "--catalog", catalogPath, "--config", catalogPath, "--server", "test-stdio", "--cli", "--method", "tools/list", ]); expectCliFailure(result); expect(result.stderr).toMatch(/mutually exclusive/); } finally { deleteConfigFile(catalogPath); } }); it("should fail when --catalog is combined with an ad-hoc target", async () => { const catalogPath = createSampleTestConfig(); const { command, args } = getTestMcpServerCommand(); try { const result = await runCli([ command, ...args, "--catalog", catalogPath, "--cli", "--method", "tools/list", ]); expectCliFailure(result); expect(result.stderr).toMatch(/--catalog cannot be combined/); } finally { deleteConfigFile(catalogPath); } }); }); describe("HTTP proxy (#2067)", () => { it("routes the MCP connection through HTTP_PROXY (#2067)", async () => { // Proxy support is one line in cli.ts — `fetch: createProxyFetch()` on the // environment — and it is what puts the proxy at the BOTTOM of the fetch // stack, where InspectorClient's own wrappers compose over it rather than // discarding it. `InspectorClient` sets `fetchFn` unconditionally, so the // transport-level fallback never fires for this client; delete that line // and CLI proxy support disappears with every core test still green. // // Asserted behaviorally, through a real forwarding proxy, rather than by // inspecting the environment object: the bug this replaces was a runtime // dispatch failure that no structural assertion would have caught. const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createEchoTool()], }); const seen: string[] = []; const proxy = createServer((req, res) => { seen.push(req.url ?? ""); const target = new URL(req.url ?? ""); const upstream = request( { host: target.hostname, port: target.port, path: target.pathname + target.search, method: req.method, headers: req.headers, }, (up) => { res.writeHead(up.statusCode ?? 502, up.headers); up.pipe(res); }, ); upstream.on("error", () => { res.writeHead(502); res.end(); }); req.pipe(upstream); }); try { await server.start(); await new Promise((r) => proxy.listen(0, "127.0.0.1", () => r())); const addr = proxy.address(); const port = typeof addr === "object" && addr !== null ? addr.port : 0; process.env.HTTP_PROXY = `http://127.0.0.1:${port}`; const result = await runCli([ server.url, "--cli", "--method", "tools/list", ]); expectCliSuccess(result); expect(JSON.stringify(expectValidJson(result))).toContain("echo"); // The point of the test: it did not go direct. expect(seen.length).toBeGreaterThan(0); expect(seen.every((u) => u.startsWith(server.url))).toBe(true); } finally { // Only this test's own variable is cleared here. The ambient ones are // restored by the file-level afterAll, so the memoized agent can never // be left bound to this now-closed proxy for a later test. delete process.env.HTTP_PROXY; await new Promise((r) => proxy.close(() => r())); await server.stop(); } }); }); describe("Roots capability (#1797)", () => { it("answers a server's roots/list instead of -32601", async () => { // The CLI used to omit `roots` when constructing its InspectorClient, so // the capability was never advertised and no `roots/list` handler was // registered — a server that asked got -32601 Method not found. (And // `--method roots/set` could not announce the change: the SDK refuses // `roots/list_changed` from a client that never declared it, so nothing // reached the wire.) `cli.ts` now always passes the `roots` option — // empty on this ad-hoc path, since there is no config file. const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createListRootsTool()], }); try { await server.start(); const result = await runCli([ server.url, "--cli", "--method", "tools/call", "--tool-name", "list_roots", ]); expectCliSuccess(result); const json = expectValidJson(result); // The tool asks the client for roots and renders what it got. An // unregistered handler surfaces as an isError result quoting -32601. expect(json.isError).toBeFalsy(); expect(JSON.stringify(json)).toContain("Roots:"); expect(JSON.stringify(json)).not.toContain("-32601"); } finally { await server.stop(); } }); it("answers with the roots configured for the server in the config file", async () => { // Answering `roots/list` is only half the fix: the roots a user // configured in mcp.json have to be the answer. They ride in on // `serverSettings.roots` (lifted by `mcpConfigToServerEntries`), which is // what web passes too — a CLI that seeded `[]` would let a server fall // back to its own defaults, the outcome #1797 is about. const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createListRootsTool()], }); let configPath: string | undefined; try { await server.start(); configPath = createTestConfig({ mcpServers: { web: { type: "streamable-http", url: server.url, roots: [{ uri: "file:///configured", name: "Configured" }], } as unknown as MCPServerConfig, }, }); const result = await runCli([ "--config", configPath, "--server", "web", "--cli", "--method", "tools/call", "--tool-name", "list_roots", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json.isError).toBeFalsy(); expect(JSON.stringify(json)).toContain("file:///configured"); } finally { await server.stop(); if (configPath) deleteConfigFile(configPath); } }); }); describe("Config-file settings lifting (#1482)", () => { it("applies a config file's custom header on a tools/call over HTTP", async () => { const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createEchoTool()], }); let configPath: string | undefined; try { await server.start(); // Write a config whose server carries disk-level headers + a timeout. // `headers`/`requestTimeout` are Inspector-extension fields not present // on MCPServerConfig, so build the entry as a loose record. configPath = createTestConfig({ mcpServers: { web: { type: "streamable-http", url: server.url, headers: { "X-Custom-Token": "secret-123" }, requestTimeout: 8000, } as unknown as MCPServerConfig, }, }); const result = await runCli([ "--config", configPath, "--server", "web", "--cli", "--method", "tools/call", "--tool-name", "echo", "--tool-arg", "message=hi", ]); expectCliSuccess(result); // The server should have received a request carrying the disk header // (Express lower-cases header names), proving the CLI lifted it from // the config file into the connection's settings. const sawHeader = server .getRecordedRequests() .some((r) => r.headers?.["x-custom-token"] === "secret-123"); expect(sawHeader).toBe(true); } finally { await server.stop(); if (configPath) deleteConfigFile(configPath); } }); it("overrides a config file's header with --header", async () => { const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createEchoTool()], }); let configPath: string | undefined; try { await server.start(); configPath = createTestConfig({ mcpServers: { web: { type: "streamable-http", url: server.url, headers: { "X-Custom-Token": "from-disk" }, } as unknown as MCPServerConfig, }, }); const result = await runCli([ "--config", configPath, "--server", "web", "--header", "X-Custom-Token: from-cli", "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const tokens = server .getRecordedRequests() .map((r) => r.headers?.["x-custom-token"]); expect(tokens).toContain("from-cli"); expect(tokens).not.toContain("from-disk"); } finally { await server.stop(); if (configPath) deleteConfigFile(configPath); } }); }); describe("Resource Options", () => { it("should read resource with URI", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "--cli", "--method", "resources/read", "--uri", "demo://resource/static/document/architecture.md", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("contents"); expect(Array.isArray(json.contents)).toBe(true); expect(json.contents.length).toBeGreaterThan(0); expect(json.contents[0]).toHaveProperty( "uri", "demo://resource/static/document/architecture.md", ); expect(json.contents[0]).toHaveProperty("mimeType", "text/markdown"); expect(json.contents[0]).toHaveProperty("text"); expect(json.contents[0].text).toContain("Architecture Documentation"); }); it("should fail when reading resource without URI", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "--cli", "--method", "resources/read", ]); expectCliFailure(result); }); }); describe("Prompt Options", () => { it("should get prompt by name", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "--cli", "--method", "prompts/get", "--prompt-name", "simple_prompt", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("messages"); expect(Array.isArray(json.messages)).toBe(true); expect(json.messages.length).toBeGreaterThan(0); expect(json.messages[0]).toHaveProperty("role", "user"); expect(json.messages[0]).toHaveProperty("content"); expect(json.messages[0].content).toHaveProperty("type", "text"); expect(json.messages[0].content.text).toBe( "This is a simple prompt for testing purposes.", ); }); it("should get prompt with arguments", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "--cli", "--method", "prompts/get", "--prompt-name", "args_prompt", "--prompt-args", "city=New York", "state=NY", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("messages"); expect(Array.isArray(json.messages)).toBe(true); expect(json.messages.length).toBeGreaterThan(0); expect(json.messages[0]).toHaveProperty("role", "user"); expect(json.messages[0]).toHaveProperty("content"); expect(json.messages[0].content).toHaveProperty("type", "text"); // Verify that the arguments were actually used in the response expect(json.messages[0].content.text).toContain("city=New York"); expect(json.messages[0].content.text).toContain("state=NY"); }); it("should fail when getting prompt without name", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "--cli", "--method", "prompts/get", ]); expectCliFailure(result); }); }); describe("Logging Options", () => { it("should set log level", async () => { const server = createTestServerHttp({ serverInfo: createTestServerInfo(), logging: true, }); try { await server.start(); const result = await runCli([ server.url, "--cli", "--method", "logging/setLevel", "--log-level", "debug", "--transport", "http", ]); expectCliSuccess(result); // Validate the response - logging/setLevel should return an empty result const json = expectValidJson(result); expect(json).toEqual({}); // Validate that the server actually received and recorded the log level expect(server.getCurrentLogLevel()).toBe("debug"); } finally { await server.stop(); } }); it("should reject invalid log level", async () => { const { command, args } = getTestMcpServerCommand(); const result = await runCli([ command, ...args, "--cli", "--method", "logging/setLevel", "--log-level", "invalid", ]); expectCliFailure(result); }); }); describe("Combined Options", () => { it("should handle config file with environment variables", async () => { const configPath = createSampleTestConfig(); try { const result = await runCli([ "--config", configPath, "--server", "test-stdio", "-e", "CLI_ENV_VAR=cli_value", "--cli", "--method", "resources/read", "--uri", "test://env", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("contents"); expect(Array.isArray(json.contents)).toBe(true); expect(json.contents.length).toBeGreaterThan(0); // Parse the env vars from the resource const envVars = JSON.parse(json.contents[0].text); expect(envVars).toHaveProperty("CLI_ENV_VAR"); expect(envVars.CLI_ENV_VAR).toBe("cli_value"); } finally { deleteConfigFile(configPath); } }); it("should handle all options together", async () => { const configPath = createSampleTestConfig(); try { const result = await runCli([ "--config", configPath, "--server", "test-stdio", "-e", "CLI_ENV_VAR=cli_value", "--cli", "--method", "tools/call", "--tool-name", "echo", "--tool-arg", "message=Hello", "--log-level", "debug", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("content"); expect(Array.isArray(json.content)).toBe(true); expect(json.content.length).toBeGreaterThan(0); expect(json.content[0]).toHaveProperty("type", "text"); expect(json.content[0].text).toBe("Echo: Hello"); } finally { deleteConfigFile(configPath); } }); }); describe("Config Transport Types", () => { it("should work with stdio transport type", async () => { const { command, args } = getTestMcpServerCommand(); const configPath = createTestConfig({ mcpServers: { "test-stdio": { type: "stdio", command, args, env: { TEST_ENV: "test-value", }, }, }, }); try { // First validate tools/list works const toolsResult = await runCli([ "--config", configPath, "--server", "test-stdio", "--cli", "--method", "tools/list", ]); expectCliSuccess(toolsResult); const toolsJson = expectValidJson(toolsResult); expect(toolsJson).toHaveProperty("tools"); expect(Array.isArray(toolsJson.tools)).toBe(true); expect(toolsJson.tools.length).toBeGreaterThan(0); // Then validate env vars from config are passed to server const envResult = await runCli([ "--config", configPath, "--server", "test-stdio", "--cli", "--method", "resources/read", "--uri", "test://env", ]); expectCliSuccess(envResult); const envJson = expectValidJson(envResult); const envVars = JSON.parse(envJson.contents[0].text); expect(envVars).toHaveProperty("TEST_ENV"); expect(envVars.TEST_ENV).toBe("test-value"); } finally { deleteConfigFile(configPath); } }); it("should fail with SSE transport type in CLI mode (connection error)", async () => { const configPath = createTestConfig({ mcpServers: { "test-sse": { type: "sse", url: "http://localhost:3000/sse", }, }, }); try { const result = await runCli([ "--config", configPath, "--server", "test-sse", "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { deleteConfigFile(configPath); } }); it("should fail with HTTP transport type in CLI mode (connection error)", async () => { const configPath = createTestConfig({ mcpServers: { "test-http": { type: "streamable-http", url: "http://localhost:3001/mcp", }, }, }); try { const result = await runCli([ "--config", configPath, "--server", "test-http", "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { deleteConfigFile(configPath); } }); it("should work with legacy config without type field", async () => { const { command, args } = getTestMcpServerCommand(); const configPath = createTestConfig({ mcpServers: { "test-legacy": { command, args, env: { LEGACY_ENV: "legacy-value", }, }, }, }); try { // First validate tools/list works const toolsResult = await runCli([ "--config", configPath, "--server", "test-legacy", "--cli", "--method", "tools/list", ]); expectCliSuccess(toolsResult); const toolsJson = expectValidJson(toolsResult); expect(toolsJson).toHaveProperty("tools"); expect(Array.isArray(toolsJson.tools)).toBe(true); expect(toolsJson.tools.length).toBeGreaterThan(0); // Then validate env vars from config are passed to server const envResult = await runCli([ "--config", configPath, "--server", "test-legacy", "--cli", "--method", "resources/read", "--uri", "test://env", ]); expectCliSuccess(envResult); const envJson = expectValidJson(envResult); const envVars = JSON.parse(envJson.contents[0].text); expect(envVars).toHaveProperty("LEGACY_ENV"); expect(envVars.LEGACY_ENV).toBe("legacy-value"); } finally { deleteConfigFile(configPath); } }); }); describe("Default Server Selection", () => { it("should auto-select single server", async () => { const { command, args } = getTestMcpServerCommand(); const configPath = createTestConfig({ mcpServers: { "only-server": { command, args, }, }, }); try { const result = await runCli([ "--config", configPath, "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("tools"); expect(Array.isArray(json.tools)).toBe(true); expect(json.tools.length).toBeGreaterThan(0); } finally { deleteConfigFile(configPath); } }); it("should require explicit server selection even with default-server key (multiple servers)", async () => { const { command, args } = getTestMcpServerCommand(); const configPath = createTestConfig({ mcpServers: { "default-server": { command, args, }, "other-server": { command: "node", args: ["other.js"], }, }, }); try { const result = await runCli([ "--config", configPath, "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { deleteConfigFile(configPath); } }); it("should require explicit server selection with multiple servers", async () => { const { command, args } = getTestMcpServerCommand(); const configPath = createTestConfig({ mcpServers: { server1: { command, args, }, server2: { command: "node", args: ["other.js"], }, }, }); try { const result = await runCli([ "--config", configPath, "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { deleteConfigFile(configPath); } }); }); describe("HTTP Transport", () => { it("should infer HTTP transport from URL ending with /mcp", async () => { const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createEchoTool()], }); try { await server.start(); const result = await runCli([ server.url, "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("tools"); expect(Array.isArray(json.tools)).toBe(true); expect(json.tools.length).toBeGreaterThan(0); } finally { await server.stop(); } }); it("should work with explicit --transport http flag", async () => { const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createEchoTool()], }); try { await server.start(); const result = await runCli([ server.url, "--transport", "http", "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("tools"); expect(Array.isArray(json.tools)).toBe(true); expect(json.tools.length).toBeGreaterThan(0); } finally { await server.stop(); } }); it("should work with explicit transport flag and URL suffix", async () => { const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createEchoTool()], }); try { await server.start(); const result = await runCli([ server.url, "--transport", "http", "--cli", "--method", "tools/list", ]); expectCliSuccess(result); const json = expectValidJson(result); expect(json).toHaveProperty("tools"); expect(Array.isArray(json.tools)).toBe(true); expect(json.tools.length).toBeGreaterThan(0); } finally { await server.stop(); } }); it("should fail when SSE transport is given to HTTP server", async () => { const server = createTestServerHttp({ serverInfo: createTestServerInfo(), tools: [createEchoTool()], }); try { await server.start(); const result = await runCli([ server.url, "--transport", "sse", "--cli", "--method", "tools/list", ]); expectCliFailure(result); } finally { await server.stop(); } }); it("should fail when HTTP transport is specified without URL", async () => { const result = await runCli([ "--transport", "http", "--cli", "--method", "tools/list", ]); expectCliFailure(result); }); it("should fail when SSE transport is specified without URL", async () => { const result = await runCli([ "--transport", "sse", "--cli", "--method", "tools/list", ]); expectCliFailure(result); }); }); describe("argv placeholder fallbacks", () => { // The cli-runner helper always prepends `["node", "inspector-cli", ...]`, so // `rawArgs[0]`/`rawArgs[1]` are never undefined through it. Call `runCli` // directly with a too-short argv to exercise the `?? "node"` / // `?? "inspector-cli"` fallbacks in parseArgs (commander's `name()`/program // bootstrap). With no method present, parseArgs throws "Method is required" // before any connection, so this stays fully in-process and fast. it("defaults the node/script argv slots when rawArgs has fewer than two entries", async () => { // Point the catalog at a fresh temp file so the empty-argv run resolves // deterministically (the default ~/.mcp-inspector catalog may already hold // several servers on the dev machine). Either way the rawArgs[0]/[1] // fallbacks have already run by the time parseArgs throws. const tempCatalog = join( mkdtempSync(join(tmpdir(), "cli-argv-")), "mcp.json", ); const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); const prev = process.env.MCP_CATALOG_PATH; process.env.MCP_CATALOG_PATH = tempCatalog; try { await expect(runCliDirect([])).rejects.toThrow(); } finally { if (prev === undefined) delete process.env.MCP_CATALOG_PATH; else process.env.MCP_CATALOG_PATH = prev; errorSpy.mockRestore(); } }); }); });