Skip to content

Commit 5ca3aab

Browse files
authored
Merge pull request modelcontextprotocol#716 from philfreo/feature/add-header-support
Add support for --header flags in MCP Inspector CLI
2 parents ef11c36 + 897d9f0 commit 5ca3aab

File tree

6 files changed

+117
-9
lines changed

6 files changed

+117
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ sdk
1515
client/playwright-report/
1616
client/results.json
1717
client/test-results/
18+
client/e2e/test-results/
1819
mcp.json

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ server/build
33
CODE_OF_CONDUCT.md
44
SECURITY.md
55
mcp.json
6+
.claude/settings.local.json

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,9 @@ npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com
410410
# Connect to a remote MCP server (with Streamable HTTP transport)
411411
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list
412412

413+
# Connect to a remote MCP server (with custom headers)
414+
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --transport http --method tools/list --header "X-API-Key: your-api-key"
415+
413416
# Call a tool on a remote server
414417
npx @modelcontextprotocol/inspector --cli https://my-mcp-server.example.com --method tools/call --tool-name remotetool --tool-arg param=value
415418

cli/src/cli.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Args = {
1616
cli: boolean;
1717
transport?: "stdio" | "sse" | "streamable-http";
1818
serverUrl?: string;
19+
headers?: Record<string, string>;
1920
};
2021

2122
type CliOptions = {
@@ -25,6 +26,7 @@ type CliOptions = {
2526
cli?: boolean;
2627
transport?: string;
2728
serverUrl?: string;
29+
header?: Record<string, string>;
2830
};
2931

3032
type ServerConfig =
@@ -127,6 +129,9 @@ async function runCli(args: Args): Promise<void> {
127129
// Build CLI arguments
128130
const cliArgs = [cliPath];
129131

132+
// Add target URL/command first
133+
cliArgs.push(args.command, ...args.args);
134+
130135
// Add transport flag if specified
131136
if (args.transport && args.transport !== "stdio") {
132137
// Convert streamable-http back to http for CLI mode
@@ -135,8 +140,12 @@ async function runCli(args: Args): Promise<void> {
135140
cliArgs.push("--transport", cliTransport);
136141
}
137142

138-
// Add command and remaining args
139-
cliArgs.push(args.command, ...args.args);
143+
// Add headers if specified
144+
if (args.headers) {
145+
for (const [key, value] of Object.entries(args.headers)) {
146+
cliArgs.push("--header", `${key}: ${value}`);
147+
}
148+
}
140149

141150
await spawnPromise("node", cliArgs, {
142151
env: { ...process.env, ...args.envArgs },
@@ -201,6 +210,30 @@ function parseKeyValuePair(
201210
return { ...previous, [key as string]: val };
202211
}
203212

213+
function parseHeaderPair(
214+
value: string,
215+
previous: Record<string, string> = {},
216+
): Record<string, string> {
217+
const colonIndex = value.indexOf(":");
218+
219+
if (colonIndex === -1) {
220+
throw new Error(
221+
`Invalid header format: ${value}. Use "HeaderName: Value" format.`,
222+
);
223+
}
224+
225+
const key = value.slice(0, colonIndex).trim();
226+
const val = value.slice(colonIndex + 1).trim();
227+
228+
if (key === "" || val === "") {
229+
throw new Error(
230+
`Invalid header format: ${value}. Use "HeaderName: Value" format.`,
231+
);
232+
}
233+
234+
return { ...previous, [key]: val };
235+
}
236+
204237
function parseArgs(): Args {
205238
const program = new Command();
206239

@@ -227,7 +260,13 @@ function parseArgs(): Args {
227260
.option("--server <n>", "server name from config file")
228261
.option("--cli", "enable CLI mode")
229262
.option("--transport <type>", "transport type (stdio, sse, http)")
230-
.option("--server-url <url>", "server URL for SSE/HTTP transport");
263+
.option("--server-url <url>", "server URL for SSE/HTTP transport")
264+
.option(
265+
"--header <headers...>",
266+
'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
267+
parseHeaderPair,
268+
{},
269+
);
231270

232271
// Parse only the arguments before --
233272
program.parse(preArgs);
@@ -280,6 +319,7 @@ function parseArgs(): Args {
280319
envArgs: { ...(config.env || {}), ...(options.e || {}) },
281320
cli: options.cli || false,
282321
transport: "stdio",
322+
headers: options.header,
283323
};
284324
} else if (config.type === "sse" || config.type === "streamable-http") {
285325
return {
@@ -289,6 +329,7 @@ function parseArgs(): Args {
289329
cli: options.cli || false,
290330
transport: config.type,
291331
serverUrl: config.url,
332+
headers: options.header,
292333
};
293334
} else {
294335
// Backwards compatibility: if no type field, assume stdio
@@ -298,6 +339,7 @@ function parseArgs(): Args {
298339
envArgs: { ...((config as any).env || {}), ...(options.e || {}) },
299340
cli: options.cli || false,
300341
transport: "stdio",
342+
headers: options.header,
301343
};
302344
}
303345
}
@@ -319,6 +361,7 @@ function parseArgs(): Args {
319361
cli: options.cli || false,
320362
transport: transport as "stdio" | "sse" | "streamable-http" | undefined,
321363
serverUrl: options.serverUrl,
364+
headers: options.header,
322365
};
323366
}
324367

cli/src/index.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ type Args = {
4040
toolName?: string;
4141
toolArg?: Record<string, JsonValue>;
4242
transport?: "sse" | "stdio" | "http";
43+
headers?: Record<string, string>;
4344
};
4445

4546
function createTransportOptions(
4647
target: string[],
4748
transport?: "sse" | "stdio" | "http",
49+
headers?: Record<string, string>,
4850
): TransportOptions {
4951
if (target.length === 0) {
5052
throw new Error(
@@ -91,11 +93,16 @@ function createTransportOptions(
9193
command: isUrl ? undefined : command,
9294
args: isUrl ? undefined : commandArgs,
9395
url: isUrl ? command : undefined,
96+
headers,
9497
};
9598
}
9699

97100
async function callMethod(args: Args): Promise<void> {
98-
const transportOptions = createTransportOptions(args.target, args.transport);
101+
const transportOptions = createTransportOptions(
102+
args.target,
103+
args.transport,
104+
args.headers,
105+
);
99106
const transport = createTransport(transportOptions);
100107
const client = new Client({
101108
name: "inspector-cli",
@@ -196,6 +203,30 @@ function parseKeyValuePair(
196203
return { ...previous, [key as string]: parsedValue };
197204
}
198205

206+
function parseHeaderPair(
207+
value: string,
208+
previous: Record<string, string> = {},
209+
): Record<string, string> {
210+
const colonIndex = value.indexOf(":");
211+
212+
if (colonIndex === -1) {
213+
throw new Error(
214+
`Invalid header format: ${value}. Use "HeaderName: Value" format.`,
215+
);
216+
}
217+
218+
const key = value.slice(0, colonIndex).trim();
219+
const val = value.slice(colonIndex + 1).trim();
220+
221+
if (key === "" || val === "") {
222+
throw new Error(
223+
`Invalid header format: ${value}. Use "HeaderName: Value" format.`,
224+
);
225+
}
226+
227+
return { ...previous, [key]: val };
228+
}
229+
199230
function parseArgs(): Args {
200231
const program = new Command();
201232

@@ -275,12 +306,24 @@ function parseArgs(): Args {
275306
}
276307
return value as "sse" | "http" | "stdio";
277308
},
309+
)
310+
//
311+
// HTTP headers
312+
//
313+
.option(
314+
"--header <headers...>",
315+
'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
316+
parseHeaderPair,
317+
{},
278318
);
279319

280320
// Parse only the arguments before --
281321
program.parse(preArgs);
282322

283-
const options = program.opts() as Omit<Args, "target">;
323+
const options = program.opts() as Omit<Args, "target"> & {
324+
header?: Record<string, string>;
325+
};
326+
284327
let remainingArgs = program.args;
285328

286329
// Add back any arguments that came after --
@@ -295,6 +338,7 @@ function parseArgs(): Args {
295338
return {
296339
target: finalArgs,
297340
...options,
341+
headers: options.header, // commander.js uses 'header' field, map to 'headers'
298342
};
299343
}
300344

@@ -306,8 +350,9 @@ async function main(): Promise<void> {
306350
try {
307351
const args = parseArgs();
308352
await callMethod(args);
309-
// Explicitly exit to ensure process terminates in CI
310-
process.exit(0);
353+
354+
// Let Node.js naturally exit instead of force-exiting
355+
// process.exit(0) was causing stdout truncation
311356
} catch (error) {
312357
handleError(error);
313358
}

cli/src/transport.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type TransportOptions = {
1212
command?: string;
1313
args?: string[];
1414
url?: string;
15+
headers?: Record<string, string>;
1516
};
1617

1718
function createStdioTransport(options: TransportOptions): Transport {
@@ -64,11 +65,25 @@ export function createTransport(options: TransportOptions): Transport {
6465
const url = new URL(options.url);
6566

6667
if (transportType === "sse") {
67-
return new SSEClientTransport(url);
68+
const transportOptions = options.headers
69+
? {
70+
requestInit: {
71+
headers: options.headers,
72+
},
73+
}
74+
: undefined;
75+
return new SSEClientTransport(url, transportOptions);
6876
}
6977

7078
if (transportType === "http") {
71-
return new StreamableHTTPClientTransport(url);
79+
const transportOptions = options.headers
80+
? {
81+
requestInit: {
82+
headers: options.headers,
83+
},
84+
}
85+
: undefined;
86+
return new StreamableHTTPClientTransport(url, transportOptions);
7287
}
7388

7489
throw new Error(`Unsupported transport type: ${transportType}`);

0 commit comments

Comments
 (0)