Skip to content
Closed
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
8 changes: 6 additions & 2 deletions clients/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --tr

When a server is loaded from a `--catalog`/`--config` file, its per-server settings (headers, connection/request timeouts, and OAuth) are applied to the connection — the same resolution the TUI uses. A `--header` flag overrides the file's headers for that run while leaving the file's timeouts and OAuth in place.

**Environment-variable semantics.** `MCP_CATALOG_PATH` is honored only when no ad-hoc target is given (positional command, `--server-url`, or `--transport`) — so a shell that exports it can still run one-off ad-hoc invocations without hitting the catalog/ad-hoc conflict. `MCP_STORAGE_DIR` sets the storage directory used by the OAuth persist backend (`<MCP_STORAGE_DIR>/oauth.json`); the per-file `MCP_INSPECTOR_OAUTH_STATE_PATH` override still takes precedence over it.

### HTTP proxy support

Connections to remote HTTP/SSE servers honor the conventional proxy environment variables: `HTTPS_PROXY` / `HTTP_PROXY` (and their lowercase forms) select the proxy, and `NO_PROXY` exempts hosts. This applies to the Node transport shared by the CLI and the web backend — no inspector-specific flag is needed. When a proxy variable is set, outbound requests are routed through undici's `EnvHttpProxyAgent`.
Expand All @@ -96,15 +98,17 @@ Options that specify the MCP server (catalog/config file, ad-hoc command/URL, en

| Option | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------- |
| `--method <method>` | MCP method to invoke (e.g. `tools/list`, `tools/call`, `resources/list`, `prompts/list`). |
| `--method <method>` | MCP method to invoke. Supports `initialize` (connect-only probe → `{serverInfo, protocolVersion, capabilities, instructions}`), `tools/list`, `tools/call`, `resources/list`, `resources/read`, `resources/templates/list`, `prompts/list`, `prompts/get`, `logging/setLevel`. |
| `--tool-name <name>` | Tool name (for `tools/call`). |
| `--tool-arg <key=value>` | Tool argument; repeat for multiple. Use `key='{"json":true}'` for JSON. |
| `--tool-arg <key=value>` | Tool argument; repeat for multiple. Use `key='{"json":true}'` for JSON. Values are coerced (JSON-parsed, so `count=1` becomes a number). |
| `--tool-args-json <json>` | Tool arguments as a single JSON object (e.g. `'{"zip":"10001"}'`). Passed verbatim — no `key=value` coercion, so `"012"` stays a string. Mutually exclusive with `--tool-arg`. |
| `--uri <uri>` | Resource URI (for `resources/read`). |
| `--prompt-name <name>` | Prompt name (for `prompts/get`). |
| `--prompt-args <key=value>` | Prompt arguments; repeat for multiple. |
| `--log-level <level>` | Logging level for `logging/setLevel` (e.g. `debug`, `info`). |
| `--metadata <key=value>` | General metadata (key=value); applied to all methods. |
| `--tool-metadata <key=value>` | Tool-specific metadata for `tools/call`. |
| `--connect-timeout <ms>` | Connection timeout in ms. Defaults to `15000` for ad-hoc `--server-url`/target runs (so a black-holed host fails fast) and to the file-level timeout for `--catalog`/`--config` runs. `0` disables the timeout. |

### CLI-specific (OAuth for HTTP servers)

Expand Down
202 changes: 202 additions & 0 deletions clients/cli/__tests__/programmatic-ergonomics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
import { describe, it, expect } from "vitest";
import { getTestMcpServerCommand } from "@modelcontextprotocol/inspector-test-server";
import { runCli } from "./helpers/cli-runner.js";
import { DEFAULT_CONNECT_TIMEOUT_MS, withConnectTimeout } from "../src/cli.js";
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
import {
expectCliFailure,
expectCliSuccess,
expectOutputContains,
} from "./helpers/assertions.js";

describe("withConnectTimeout", () => {
const baseSettings: InspectorServerSettings = {
headers: [{ key: "X-Test", value: "1" }],
metadata: [],
env: [],
connectionTimeout: 42,
requestTimeout: 0,
taskTtl: 60000,
maxFetchRequests: 1000,
autoRefreshOnListChanged: false,
roots: [],
};

it("returns the settings unchanged when no timeout is given", () => {
expect(withConnectTimeout(baseSettings, undefined)).toBe(baseSettings);
expect(withConnectTimeout(undefined, undefined)).toBeUndefined();
});

it("overrides an existing settings' connectionTimeout", () => {
const result = withConnectTimeout(baseSettings, 5000);
expect(result).not.toBe(baseSettings);
expect(result?.connectionTimeout).toBe(5000);
// Other fields are preserved.
expect(result?.headers).toEqual(baseSettings.headers);
});

it("builds a minimal settings object when none was provided", () => {
const result = withConnectTimeout(undefined, DEFAULT_CONNECT_TIMEOUT_MS);
expect(result?.connectionTimeout).toBe(DEFAULT_CONNECT_TIMEOUT_MS);
expect(result?.headers).toEqual([]);
expect(result?.requestTimeout).toBe(0);
});
});

/**
* Covers the programmatic-ergonomics flags from #1573: `--method initialize`,
* `--tool-args-json`, `--connect-timeout`, and the MCP_CATALOG_PATH ad-hoc
* gate. All connect to the bundled stdio test server.
*/
describe("--method initialize", () => {
it("connects and emits the cached InitializeResult fields", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([command, ...args, "--method", "initialize"]);
expectCliSuccess(result);
const json = JSON.parse(result.stdout) as {
serverInfo: { name: string };
protocolVersion: string;
capabilities: Record<string, unknown>;
};
expect(json.serverInfo.name).toBeTruthy();
expect(typeof json.protocolVersion).toBe("string");
expect(typeof json.capabilities).toBe("object");
});
});

describe("--tool-args-json", () => {
it("passes the JSON object verbatim (string values stay strings)", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--method",
"tools/call",
"--tool-name",
"echo",
"--tool-args-json",
JSON.stringify({ message: "012" }),
]);
expectCliSuccess(result);
// echo returns its message; with --tool-arg message=012 the value would
// coerce to the number 12 (and fail the string schema), but
// --tool-args-json preserves the string "012".
expect(result.stdout).toContain("012");
});

it("rejects --tool-args-json combined with --tool-arg", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--method",
"tools/call",
"--tool-name",
"echo",
"--tool-arg",
"x=1",
"--tool-args-json",
"{}",
]);
expectCliFailure(result);
expectOutputContains(result, "cannot be combined with --tool-arg");
});

it("rejects malformed JSON", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--method",
"tools/call",
"--tool-name",
"echo",
"--tool-args-json",
"{not json}",
]);
expectCliFailure(result);
expectOutputContains(result, "not valid JSON");
});

it("rejects a non-object value", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--method",
"tools/call",
"--tool-name",
"echo",
"--tool-args-json",
"[1,2]",
]);
expectCliFailure(result);
expectOutputContains(result, "must be a JSON object");
});
});

describe("MCP_CATALOG_PATH with an ad-hoc target", () => {
it("does not conflict with an ad-hoc target (env catalog is ignored)", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([command, ...args, "--method", "tools/list"], {
env: { MCP_CATALOG_PATH: "/no/such/catalog.json" },
});
expectCliSuccess(result);
});
});

describe("--connect-timeout", () => {
it("accepts a numeric value and connects within it", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--connect-timeout",
"5000",
"--method",
"tools/list",
]);
expectCliSuccess(result);
});

it("accepts 0 (no timeout) and connects", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--connect-timeout",
"0",
"--method",
"tools/list",
]);
expectCliSuccess(result);
});

it("rejects a negative value", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--connect-timeout",
"-1",
"--method",
"tools/list",
]);
expectCliFailure(result);
expectOutputContains(result, "non-negative number");
});

it("rejects a non-numeric value", async () => {
const { command, args } = getTestMcpServerCommand();
const result = await runCli([
command,
...args,
"--connect-timeout",
"soon",
"--method",
"tools/list",
]);
expectCliFailure(result);
expectOutputContains(result, "non-negative number");
});
});
Loading
Loading