Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions clients/cli/__tests__/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
const result = await runCli(["--completion", shell]);
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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", "--"]),
Expand Down
11 changes: 4 additions & 7 deletions clients/cli/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Command } from "commander";
import {
CATALOG_METHODS,
emitCompletionIfRequested,
isCatalogMethod,
registerCompletionOption,
} from "./completion.js";
type McpResponse = Record<string, unknown>;
Expand All @@ -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";
Expand Down Expand Up @@ -1268,13 +1269,9 @@ async function parseArgs(argv?: string[]): Promise<ParseResult> {
"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") {
Expand Down
19 changes: 17 additions & 2 deletions clients/cli/src/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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
Expand All @@ -51,6 +65,7 @@ export const VALUE_CHOICES: Readonly<Record<string, readonly string[]>> = {
"--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,
};
Expand Down
Loading