From 77d4963b8a016ef13c7d8b526e352818e556c482 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 11:57:31 -0400 Subject: [PATCH 1/4] Add support for --header flags in MCP Inspector CLI - Add --header option to both wrapper CLI (cli.ts) and main CLI (index.ts) - Support multiple headers with "HeaderName: Value" format validation - Pass headers to HTTP and SSE transports via requestInit.headers - Add parseHeaderPair function for proper header parsing - Fix output truncation issue by removing forced process.exit(0) - Update types to include headers in Args and TransportOptions Usage: --header "Authorization: Bearer token" --header "X-Custom: value" --- cli/src/cli.ts | 49 +++++++++++++++++++++++++++++++++++++--- cli/src/index.ts | 53 ++++++++++++++++++++++++++++++++++++++++---- cli/src/transport.ts | 19 ++++++++++++++-- 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/cli/src/cli.ts b/cli/src/cli.ts index 13c7e492a..7188f7c8c 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -16,6 +16,7 @@ type Args = { cli: boolean; transport?: "stdio" | "sse" | "streamable-http"; serverUrl?: string; + headers?: Record; }; type CliOptions = { @@ -25,6 +26,7 @@ type CliOptions = { cli?: boolean; transport?: string; serverUrl?: string; + header?: Record; }; type ServerConfig = @@ -127,6 +129,9 @@ async function runCli(args: Args): Promise { // Build CLI arguments const cliArgs = [cliPath]; + // Add target URL/command first + cliArgs.push(args.command, ...args.args); + // Add transport flag if specified if (args.transport && args.transport !== "stdio") { // Convert streamable-http back to http for CLI mode @@ -135,8 +140,12 @@ async function runCli(args: Args): Promise { cliArgs.push("--transport", cliTransport); } - // Add command and remaining args - cliArgs.push(args.command, ...args.args); + // Add headers if specified + if (args.headers) { + for (const [key, value] of Object.entries(args.headers)) { + cliArgs.push("--header", `${key}: ${value}`); + } + } await spawnPromise("node", cliArgs, { env: { ...process.env, ...args.envArgs }, @@ -201,6 +210,30 @@ function parseKeyValuePair( return { ...previous, [key as string]: val }; } +function parseHeaderPair( + value: string, + previous: Record = {}, +): Record { + const colonIndex = value.indexOf(":"); + + if (colonIndex === -1) { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + const key = value.slice(0, colonIndex).trim(); + const val = value.slice(colonIndex + 1).trim(); + + if (key === "" || val === "") { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + return { ...previous, [key]: val }; +} + function parseArgs(): Args { const program = new Command(); @@ -227,7 +260,13 @@ function parseArgs(): Args { .option("--server ", "server name from config file") .option("--cli", "enable CLI mode") .option("--transport ", "transport type (stdio, sse, http)") - .option("--server-url ", "server URL for SSE/HTTP transport"); + .option("--server-url ", "server URL for SSE/HTTP transport") + .option( + "--header ", + 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', + parseHeaderPair, + {}, + ); // Parse only the arguments before -- program.parse(preArgs); @@ -280,6 +319,7 @@ function parseArgs(): Args { envArgs: { ...(config.env || {}), ...(options.e || {}) }, cli: options.cli || false, transport: "stdio", + headers: options.header, }; } else if (config.type === "sse" || config.type === "streamable-http") { return { @@ -289,6 +329,7 @@ function parseArgs(): Args { cli: options.cli || false, transport: config.type, serverUrl: config.url, + headers: options.header, }; } else { // Backwards compatibility: if no type field, assume stdio @@ -298,6 +339,7 @@ function parseArgs(): Args { envArgs: { ...((config as any).env || {}), ...(options.e || {}) }, cli: options.cli || false, transport: "stdio", + headers: options.header, }; } } @@ -319,6 +361,7 @@ function parseArgs(): Args { cli: options.cli || false, transport: transport as "stdio" | "sse" | "streamable-http" | undefined, serverUrl: options.serverUrl, + headers: options.header, }; } diff --git a/cli/src/index.ts b/cli/src/index.ts index 2b0c4f53d..9bd258de0 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -30,11 +30,13 @@ type Args = { toolName?: string; toolArg?: Record; transport?: "sse" | "stdio" | "http"; + headers?: Record; }; function createTransportOptions( target: string[], transport?: "sse" | "stdio" | "http", + headers?: Record, ): TransportOptions { if (target.length === 0) { throw new Error( @@ -81,11 +83,16 @@ function createTransportOptions( command: isUrl ? undefined : command, args: isUrl ? undefined : commandArgs, url: isUrl ? command : undefined, + headers, }; } async function callMethod(args: Args): Promise { - const transportOptions = createTransportOptions(args.target, args.transport); + const transportOptions = createTransportOptions( + args.target, + args.transport, + args.headers, + ); const transport = createTransport(transportOptions); const client = new Client({ name: "inspector-cli", @@ -177,6 +184,30 @@ function parseKeyValuePair( return { ...previous, [key as string]: val }; } +function parseHeaderPair( + value: string, + previous: Record = {}, +): Record { + const colonIndex = value.indexOf(":"); + + if (colonIndex === -1) { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + const key = value.slice(0, colonIndex).trim(); + const val = value.slice(colonIndex + 1).trim(); + + if (key === "" || val === "") { + throw new Error( + `Invalid header format: ${value}. Use "HeaderName: Value" format.`, + ); + } + + return { ...previous, [key]: val }; +} + function parseArgs(): Args { const program = new Command(); @@ -256,12 +287,24 @@ function parseArgs(): Args { } return value as "sse" | "http" | "stdio"; }, + ) + // + // HTTP headers + // + .option( + "--header ", + 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)', + parseHeaderPair, + {}, ); // Parse only the arguments before -- program.parse(preArgs); - const options = program.opts() as Omit; + const options = program.opts() as Omit & { + header?: Record; + }; + let remainingArgs = program.args; // Add back any arguments that came after -- @@ -276,6 +319,7 @@ function parseArgs(): Args { return { target: finalArgs, ...options, + headers: options.header, // commander.js uses 'header' field, map to 'headers' }; } @@ -287,8 +331,9 @@ async function main(): Promise { try { const args = parseArgs(); await callMethod(args); - // Explicitly exit to ensure process terminates in CI - process.exit(0); + + // Let Node.js naturally exit instead of force-exiting + // process.exit(0) was causing stdout truncation } catch (error) { handleError(error); } diff --git a/cli/src/transport.ts b/cli/src/transport.ts index b6276356d..3ed6e31ae 100644 --- a/cli/src/transport.ts +++ b/cli/src/transport.ts @@ -12,6 +12,7 @@ export type TransportOptions = { command?: string; args?: string[]; url?: string; + headers?: Record; }; function createStdioTransport(options: TransportOptions): Transport { @@ -64,11 +65,25 @@ export function createTransport(options: TransportOptions): Transport { const url = new URL(options.url); if (transportType === "sse") { - return new SSEClientTransport(url); + const transportOptions = options.headers + ? { + requestInit: { + headers: options.headers, + }, + } + : undefined; + return new SSEClientTransport(url, transportOptions); } if (transportType === "http") { - return new StreamableHTTPClientTransport(url); + const transportOptions = options.headers + ? { + requestInit: { + headers: options.headers, + }, + } + : undefined; + return new StreamableHTTPClientTransport(url, transportOptions); } throw new Error(`Unsupported transport type: ${transportType}`); From 755f31f10972ec12c4de110246f3ad284a7374c5 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 12:06:31 -0400 Subject: [PATCH 2/4] Add Claude settings to prettierignore --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index cd910373e..167a7cb48 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,3 +3,4 @@ server/build CODE_OF_CONDUCT.md SECURITY.md mcp.json +.claude/settings.local.json \ No newline at end of file From da5595fb3ac5599b0f069393fcd7c8095de23983 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 12:08:04 -0400 Subject: [PATCH 3/4] Add client/e2e/test-results/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ce556395..c63eadaa4 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ sdk client/playwright-report/ client/results.json client/test-results/ +client/e2e/test-results/ mcp.json From d7927ec8dc13c1d39039673b1e9302972875c719 Mon Sep 17 00:00:00 2001 From: Phil Freo Date: Thu, 14 Aug 2025 12:12:43 -0400 Subject: [PATCH 4/4] Add documentation for CLI header support - Document --header flag usage with examples - Explain header format requirements - Clarify transport support (HTTP, SSE, not STDIO) - Add authentication examples with API keys and bearer tokens --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ed1e50931..02a096e2a 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,9 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com # Connect to a remote MCP server (with Streamable HTTP transport) npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list +# Connect to a remote MCP server (with custom headers) +npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header "X-API-Key: your-api-key" + # Call a tool on a remote server npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value