diff --git a/clients/cli/__tests__/completion.test.ts b/clients/cli/__tests__/completion.test.ts index 66951750f..ffa3414ac 100644 --- a/clients/cli/__tests__/completion.test.ts +++ b/clients/cli/__tests__/completion.test.ts @@ -24,6 +24,7 @@ import { type CompletionShell, } from "../src/completion.js"; import { ONE_SHOT_METHODS } from "@inspector/core/cli/handlers/method-types.js"; +import { CATALOG_WRITE_METHODS } from "../src/handlers/servers-write.js"; async function script(shell: CompletionShell): Promise { const result = await runCli(["--completion", shell]); @@ -94,6 +95,28 @@ describe("--completion", () => { expect(out).toContain(method); } }); + + it("offers the catalog write methods the CLI implements (#2629)", async () => { + // Read from servers-write itself, not from CATALOG_METHODS, so a write + // method the completion list forgets fails here. + expect(CATALOG_METHODS).toEqual( + expect.arrayContaining([...CATALOG_WRITE_METHODS]), + ); + const out = await script("bash"); + for (const method of CATALOG_WRITE_METHODS) { + expect(out).toContain(method); + } + }); + + it("offers no --method the CLI would reject as unsupported", async () => { + for (const method of VALUE_CHOICES["--method"]!) { + const result = await runCli(["--method", method]); + expect(result.stderr, method).not.toContain("Unsupported method"); + } + // Control: an unknown method is rejected by the same check. + const bogus = await runCli(["--method", "servers/bogus"]); + expect(bogus.stderr).toContain("Unsupported method: servers/bogus"); + }); }); describe("collectCompletionFlags", () => { @@ -227,6 +250,18 @@ describe.skipIf(!hasShell("bash"))("bash script", () => { expect( await complete(["mcp-inspector", "--cli", "--transport", ""]), ).toEqual(["stdio", "sse", "http"]); + expect( + await complete(["mcp-inspector", "--cli", "--method", "servers/"]), + ).toEqual([ + "servers/list", + "servers/show", + "servers/add", + "servers/edit", + "servers/remove", + ]); + expect( + await complete(["mcp-inspector", "--cli", "--output-format", ""]), + ).toEqual(["raw", "json"]); // A free-form value offers nothing (the shell falls back to files). expect( await complete(["mcp-inspector", "--cli", "--tool-name", "--"]), diff --git a/clients/cli/src/cli.ts b/clients/cli/src/cli.ts index 25f55d537..34653b7f1 100644 --- a/clients/cli/src/cli.ts +++ b/clients/cli/src/cli.ts @@ -1,6 +1,8 @@ import { Command } from "commander"; import { + CATALOG_METHODS, emitCompletionIfRequested, + isCatalogMethod, registerCompletionOption, } from "./completion.js"; type McpResponse = Record; @@ -21,7 +23,6 @@ import { showServerEntry, } from "@inspector/core/cli/handlers/servers-list.js"; import { - CATALOG_WRITE_METHODS, isCatalogWriteMethod, runCatalogWrite, } from "./handlers/servers-write.js"; @@ -1268,13 +1269,9 @@ async function parseArgs(argv?: string[]): Promise { "Method is required. Use --method to specify the method to invoke.", ); } - const isCatalogMethod = - options.method === "servers/list" || - options.method === "servers/show" || - isCatalogWriteMethod(options.method); - if (!isCatalogMethod && !isOneShotMethod(options.method)) { + if (!isCatalogMethod(options.method) && !isOneShotMethod(options.method)) { throw new Error( - `Unsupported method: ${options.method}. Supported --cli methods: ${ONE_SHOT_METHODS.join(", ")}, servers/list, servers/show, ${CATALOG_WRITE_METHODS.join(", ")}.`, + `Unsupported method: ${options.method}. Supported --cli methods: ${[...ONE_SHOT_METHODS, ...CATALOG_METHODS].join(", ")}.`, ); } if (options.rename !== undefined && options.method !== "servers/edit") { diff --git a/clients/cli/src/completion.ts b/clients/cli/src/completion.ts index 11d181623..cbfb1482a 100644 --- a/clients/cli/src/completion.ts +++ b/clients/cli/src/completion.ts @@ -25,6 +25,8 @@ import type { Command, Option } from "commander"; import { LoggingLevelSchema } from "@modelcontextprotocol/core"; import { ONE_SHOT_METHODS } from "@inspector/core/cli/handlers/method-types.js"; import { awaitableLog } from "@inspector/core/cli/utils/awaitable-log.js"; +import { OUTPUT_FILE_FORMATS } from "@inspector/core/cli/handlers/output-file.js"; +import { CATALOG_WRITE_METHODS } from "./handlers/servers-write.js"; export const COMPLETION_SHELLS = ["bash", "zsh", "fish"] as const; export type CompletionShell = (typeof COMPLETION_SHELLS)[number]; @@ -39,8 +41,20 @@ const MODE_FLAGS: readonly CompletionFlag[] = [ { long: "--tui", takesValue: false, description: "Run the terminal UI" }, ]; -/** Catalog-only methods `parseArgs` accepts alongside `ONE_SHOT_METHODS`. */ -export const CATALOG_METHODS = ["servers/list", "servers/show"] as const; +/** + * Catalog-only methods `parseArgs` accepts alongside `ONE_SHOT_METHODS` — + * the reads plus the writes `servers-write` implements. `parseArgs` validates + * `--method` against this same list, so the two cannot drift (#2629). + */ +export const CATALOG_METHODS = [ + "servers/list", + "servers/show", + ...CATALOG_WRITE_METHODS, +] as const; + +export function isCatalogMethod(method: string): boolean { + return (CATALOG_METHODS as readonly string[]).includes(method); +} /** * Finite value sets for flags whose values the CLI validates in a custom @@ -51,6 +65,7 @@ export const VALUE_CHOICES: Readonly> = { "--transport": ["stdio", "sse", "http"], "--log-level": Object.values(LoggingLevelSchema.enum), "--format": ["text", "json"], + "--output-format": OUTPUT_FILE_FORMATS, "--protocol-era": ["legacy", "auto", "modern"], "--completion": COMPLETION_SHELLS, };