Merge branch 'main' into cmgriffing/fix-cli-truncated-output

This commit is contained in:
Ola Hungerford 2025-08-28 20:40:58 -07:00 • committed by GitHub
commit d3fcedc81a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 117 additions and 9 deletions

1
.gitignore vendored
View file

@ -15,4 +15,5 @@ sdk
client/playwright-report/ client/playwright-report/
client/results.json client/results.json
client/test-results/ client/test-results/
client/e2e/test-results/
mcp.json mcp.json

View file

@ -3,3 +3,4 @@ server/build
CODE_OF_CONDUCT.md CODE_OF_CONDUCT.md
SECURITY.md SECURITY.md
mcp.json mcp.json
.claude/settings.local.json

View file

@ -410,6 +410,9 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com
# Connect to a remote MCP server (with Streamable HTTP transport) # 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 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 # 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 npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value

View file

@ -16,6 +16,7 @@ type Args = {
cli: boolean; cli: boolean;
transport?: "stdio" | "sse" | "streamable-http"; transport?: "stdio" | "sse" | "streamable-http";
serverUrl?: string; serverUrl?: string;
headers?: Record<string, string>;
}; };
type CliOptions = { type CliOptions = {
@ -25,6 +26,7 @@ type CliOptions = {
cli?: boolean; cli?: boolean;
transport?: string; transport?: string;
serverUrl?: string; serverUrl?: string;
header?: Record<string, string>;
}; };
type ServerConfig = type ServerConfig =
@ -127,6 +129,9 @@ async function runCli(args: Args): Promise<void> {
// Build CLI arguments // Build CLI arguments
const cliArgs = [cliPath]; const cliArgs = [cliPath];
// Add target URL/command first
cliArgs.push(args.command, ...args.args);
// Add transport flag if specified // Add transport flag if specified
if (args.transport && args.transport !== "stdio") { if (args.transport && args.transport !== "stdio") {
// Convert streamable-http back to http for CLI mode // Convert streamable-http back to http for CLI mode
@ -135,8 +140,12 @@ async function runCli(args: Args): Promise<void> {
cliArgs.push("--transport", cliTransport); cliArgs.push("--transport", cliTransport);
} }
// Add command and remaining args // Add headers if specified
cliArgs.push(args.command, ...args.args); if (args.headers) {
for (const [key, value] of Object.entries(args.headers)) {
cliArgs.push("--header", `${key}: ${value}`);
}
}
await spawnPromise("node", cliArgs, { await spawnPromise("node", cliArgs, {
env: { ...process.env, ...args.envArgs }, env: { ...process.env, ...args.envArgs },
@ -201,6 +210,30 @@ function parseKeyValuePair(
return { ...previous, [key as string]: val }; 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 { function parseArgs(): Args {
const program = new Command(); const program = new Command();
@ -227,7 +260,13 @@ function parseArgs(): Args {
.option("--server <n>", "server name from config file") .option("--server <n>", "server name from config file")
.option("--cli", "enable CLI mode") .option("--cli", "enable CLI mode")
.option("--transport <type>", "transport type (stdio, sse, http)") .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 -- // Parse only the arguments before --
program.parse(preArgs); program.parse(preArgs);
@ -280,6 +319,7 @@ function parseArgs(): Args {
envArgs: { ...(config.env || {}), ...(options.e || {}) }, envArgs: { ...(config.env || {}), ...(options.e || {}) },
cli: options.cli || false, cli: options.cli || false,
transport: "stdio", transport: "stdio",
headers: options.header,
}; };
} else if (config.type === "sse" || config.type === "streamable-http") { } else if (config.type === "sse" || config.type === "streamable-http") {
return { return {
@ -289,6 +329,7 @@ function parseArgs(): Args {
cli: options.cli || false, cli: options.cli || false,
transport: config.type, transport: config.type,
serverUrl: config.url, serverUrl: config.url,
headers: options.header,
}; };
} else { } else {
// Backwards compatibility: if no type field, assume stdio // Backwards compatibility: if no type field, assume stdio
@ -298,6 +339,7 @@ function parseArgs(): Args {
envArgs: { ...((config as any).env || {}), ...(options.e || {}) }, envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
cli: options.cli || false, cli: options.cli || false,
transport: "stdio", transport: "stdio",
headers: options.header,
}; };
} }
} }
@ -319,6 +361,7 @@ function parseArgs(): Args {
cli: options.cli || false, cli: options.cli || false,
transport: transport as "stdio" | "sse" | "streamable-http" | undefined, transport: transport as "stdio" | "sse" | "streamable-http" | undefined,
serverUrl: options.serverUrl, serverUrl: options.serverUrl,
headers: options.header,
}; };
} }

View file

@ -41,11 +41,13 @@ type Args = {
toolName?: string; toolName?: string;
toolArg?: Record<string, JsonValue>; toolArg?: Record<string, JsonValue>;
transport?: "sse" | "stdio" | "http"; transport?: "sse" | "stdio" | "http";
headers?: Record<string, string>;
}; };
function createTransportOptions( function createTransportOptions(
target: string[], target: string[],
transport?: "sse" | "stdio" | "http", transport?: "sse" | "stdio" | "http",
headers?: Record<string, string>,
): TransportOptions { ): TransportOptions {
if (target.length === 0) { if (target.length === 0) {
throw new Error( throw new Error(
@ -92,11 +94,16 @@ function createTransportOptions(
command: isUrl ? undefined : command, command: isUrl ? undefined : command,
args: isUrl ? undefined : commandArgs, args: isUrl ? undefined : commandArgs,
url: isUrl ? command : undefined, url: isUrl ? command : undefined,
headers,
}; };
} }
async function callMethod(args: Args): Promise<void> { 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 transport = createTransport(transportOptions);
const client = new Client({ const client = new Client({
name: "inspector-cli", name: "inspector-cli",
@ -197,6 +204,30 @@ function parseKeyValuePair(
return { ...previous, [key as string]: parsedValue }; 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 { function parseArgs(): Args {
const program = new Command(); const program = new Command();
@ -276,12 +307,24 @@ function parseArgs(): Args {
} }
return value as "sse" | "http" | "stdio"; 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 -- // Parse only the arguments before --
program.parse(preArgs); 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; let remainingArgs = program.args;
// Add back any arguments that came after -- // Add back any arguments that came after --
@ -296,6 +339,7 @@ function parseArgs(): Args {
return { return {
target: finalArgs, target: finalArgs,
...options, ...options,
headers: options.header, // commander.js uses 'header' field, map to 'headers'
}; };
} }
@ -307,8 +351,9 @@ async function main(): Promise<void> {
try { try {
const args = parseArgs(); const args = parseArgs();
await callMethod(args); 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) { } catch (error) {
handleError(error); handleError(error);
} }

View file

@ -12,6 +12,7 @@ export type TransportOptions = {
command?: string; command?: string;
args?: string[]; args?: string[];
url?: string; url?: string;
headers?: Record<string, string>;
}; };
function createStdioTransport(options: TransportOptions): Transport { function createStdioTransport(options: TransportOptions): Transport {
@ -64,11 +65,25 @@ export function createTransport(options: TransportOptions): Transport {
const url = new URL(options.url); const url = new URL(options.url);
if (transportType === "sse") { if (transportType === "sse") {
return new SSEClientTransport(url); const transportOptions = options.headers
? {
requestInit: {
headers: options.headers,
},
}
: undefined;
return new SSEClientTransport(url, transportOptions);
} }
if (transportType === "http") { 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}`); throw new Error(`Unsupported transport type: ${transportType}`);