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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ sdk
client/playwright-report/
client/results.json
client/test-results/
client/e2e/test-results/
mcp.json
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ server/build
CODE_OF_CONDUCT.md
SECURITY.md
mcp.json
.claude/settings.local.json
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,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

Expand Down
49 changes: 46 additions & 3 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Args = {
cli: boolean;
transport?: "stdio" | "sse" | "streamable-http";
serverUrl?: string;
headers?: Record<string, string>;
};

type CliOptions = {
Expand All @@ -25,6 +26,7 @@ type CliOptions = {
cli?: boolean;
transport?: string;
serverUrl?: string;
header?: Record<string, string>;
};

type ServerConfig =
Expand Down Expand Up @@ -127,6 +129,9 @@ async function runCli(args: Args): Promise<void> {
// 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
Expand All @@ -135,8 +140,12 @@ async function runCli(args: Args): Promise<void> {
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 },
Expand Down Expand Up @@ -201,6 +210,30 @@ function parseKeyValuePair(
return { ...previous, [key as string]: val };
}

function parseHeaderPair(
value: string,
previous: Record<string, string> = {},
): Record<string, string> {
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();

Expand All @@ -227,7 +260,13 @@ function parseArgs(): Args {
.option("--server <n>", "server name from config file")
.option("--cli", "enable CLI mode")
.option("--transport <type>", "transport type (stdio, sse, http)")
.option("--server-url <url>", "server URL for SSE/HTTP transport");
.option("--server-url <url>", "server URL for SSE/HTTP transport")
.option(
"--header <headers...>",
'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
parseHeaderPair,
{},
);

// Parse only the arguments before --
program.parse(preArgs);
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -298,6 +339,7 @@ function parseArgs(): Args {
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
cli: options.cli || false,
transport: "stdio",
headers: options.header,
};
}
}
Expand All @@ -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,
};
}

Expand Down
53 changes: 49 additions & 4 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ type Args = {
toolName?: string;
toolArg?: Record<string, JsonValue>;
transport?: "sse" | "stdio" | "http";
headers?: Record<string, string>;
};

function createTransportOptions(
target: string[],
transport?: "sse" | "stdio" | "http",
headers?: Record<string, string>,
): TransportOptions {
if (target.length === 0) {
throw new Error(
Expand Down Expand Up @@ -91,11 +93,16 @@ function createTransportOptions(
command: isUrl ? undefined : command,
args: isUrl ? undefined : commandArgs,
url: isUrl ? command : undefined,
headers,
};
}

async function callMethod(args: Args): Promise<void> {
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",
Expand Down Expand Up @@ -196,6 +203,30 @@ function parseKeyValuePair(
return { ...previous, [key as string]: parsedValue };
}

function parseHeaderPair(
value: string,
previous: Record<string, string> = {},
): Record<string, string> {
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();

Expand Down Expand Up @@ -275,12 +306,24 @@ function parseArgs(): Args {
}
return value as "sse" | "http" | "stdio";
},
)
//
// HTTP headers
//
.option(
"--header <headers...>",
'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<Args, "target">;
const options = program.opts() as Omit<Args, "target"> & {
header?: Record<string, string>;
};

let remainingArgs = program.args;

// Add back any arguments that came after --
Expand All @@ -295,6 +338,7 @@ function parseArgs(): Args {
return {
target: finalArgs,
...options,
headers: options.header, // commander.js uses 'header' field, map to 'headers'
};
}

Expand All @@ -306,8 +350,9 @@ async function main(): Promise<void> {
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally I think this was added because it was hanging on CI, however I am not seeing that behavior in the pipeline here when the tests run.

Looking at the past commits where the process.exit(0) was added, this was the test failure that happened: https://github.com/modelcontextprotocol/inspector/actions/runs/16283307379/job/45977026872

Which doesn't happen anymore, so it may not have been needed in the first place? But we should keep an eye out for anything that might be intermittent.

} catch (error) {
handleError(error);
}
Expand Down
19 changes: 17 additions & 2 deletions cli/src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type TransportOptions = {
command?: string;
args?: string[];
url?: string;
headers?: Record<string, string>;
};

function createStdioTransport(options: TransportOptions): Transport {
Expand Down Expand Up @@ -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}`);
Expand Down