Skip to content

Commit 494f50d

Browse files
authored
Add stdio transport hardening and shared CLI transport flags (#1784)
1 parent b20bf41 commit 494f50d

10 files changed

Lines changed: 665 additions & 55 deletions

File tree

cli/src/lib/server-config.ts

Lines changed: 88 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ export interface GlobalOptions {
1212
rpc: boolean;
1313
}
1414

15+
type TransportType = "http" | "stdio";
16+
1517
export interface SharedServerTargetOptions {
18+
transport?: TransportType;
1619
url?: string;
1720
accessToken?: string;
1821
oauthAccessToken?: string;
@@ -22,8 +25,10 @@ export interface SharedServerTargetOptions {
2225
header?: string[];
2326
clientCapabilities?: string | Record<string, unknown>;
2427
command?: string;
28+
args?: string[];
2529
commandArgs?: string[];
2630
env?: string[];
31+
cwd?: string;
2732
timeout?: number;
2833
}
2934

@@ -33,6 +38,10 @@ function collectString(value: string, previous: string[] = []): string[] {
3338

3439
export function addSharedServerOptions(command: Command): Command {
3540
return command
41+
.option(
42+
"--transport <transport>",
43+
'Explicit transport type: "http" or "stdio"',
44+
)
3645
.option("--url <url>", "HTTP MCP server URL")
3746
.option("--access-token <token>", "Bearer access token for HTTP servers")
3847
.option(
@@ -62,16 +71,20 @@ export function addSharedServerOptions(command: Command): Command {
6271
"Client capabilities advertised to the server as a JSON object",
6372
)
6473
.option("--command <command>", "Command for a stdio MCP server")
74+
.option(
75+
"--args <arg...>",
76+
"Preferred stdio command arguments. Pass multiple values or repeat the flag.",
77+
)
6578
.option(
6679
"--command-args <arg>",
67-
"Stdio command argument. Repeat to pass multiple arguments.",
80+
"Legacy stdio command argument. Repeat to pass multiple arguments.",
6881
collectString,
6982
)
7083
.option(
71-
"--env <env>",
72-
'Stdio environment assignment in "KEY=VALUE" format. Repeat to pass multiple assignments.',
73-
collectString,
74-
);
84+
"-e, --env <env...>",
85+
'Stdio environment assignment in "KEY=VALUE" format. Pass multiple values or repeat the flag.',
86+
)
87+
.option("--cwd <path>", "Working directory for the stdio MCP server process");
7588
}
7689

7790
export function getGlobalOptions(command: Command): GlobalOptions {
@@ -204,21 +217,21 @@ export function parseServerConfig(
204217
const command = options.command?.trim();
205218
const hasUrl = Boolean(url);
206219
const hasCommand = Boolean(command);
220+
const transport = resolveTargetTransport(options, hasUrl, hasCommand);
221+
const cwd = options.cwd?.trim();
207222
const clientCapabilities = resolveClientCapabilities(
208223
options.clientCapabilities,
209224
);
210225

211-
if (hasUrl === hasCommand) {
212-
throw usageError("Specify exactly one target: either --url or --command.");
213-
}
214-
215-
if (hasUrl && url) {
226+
if (transport === "http" && url) {
216227
if (
228+
(options.args?.length ?? 0) > 0 ||
217229
(options.commandArgs?.length ?? 0) > 0 ||
218-
(options.env?.length ?? 0) > 0
230+
(options.env?.length ?? 0) > 0 ||
231+
cwd
219232
) {
220233
throw usageError(
221-
"--command-args and --env can only be used together with --command.",
234+
"--args, --command-args, --env, and --cwd can only be used together with --command.",
222235
);
223236
}
224237

@@ -281,10 +294,11 @@ export function parseServerConfig(
281294

282295
return {
283296
command,
284-
args: parseCommandArgs(options.commandArgs),
297+
args: parseCommandArgs(options.args, options.commandArgs),
285298
env: parseEnvironmentOption(options.env),
299+
...(cwd ? { cwd } : {}),
286300
...(clientCapabilities ? { clientCapabilities } : {}),
287-
stderr: "ignore",
301+
stderr: "pipe",
288302
timeout: options.timeout,
289303
};
290304
}
@@ -325,12 +339,17 @@ function parseHeader(entry: string): [string, string] {
325339
return [key, value];
326340
}
327341

328-
function parseCommandArgs(values: string[] | undefined): string[] | undefined {
329-
if (!values || values.length === 0) {
342+
function parseCommandArgs(
343+
values: string[] | undefined,
344+
legacyValues: string[] | undefined,
345+
): string[] | undefined {
346+
const combined = [...(values ?? []), ...(legacyValues ?? [])];
347+
348+
if (combined.length === 0) {
330349
return undefined;
331350
}
332351

333-
return values;
352+
return combined;
334353
}
335354

336355
function parseEnvironmentOption(
@@ -377,6 +396,58 @@ function resolveClientCapabilities(
377396
return parseUnknownRecord(value, "Client capabilities");
378397
}
379398

399+
function resolveTargetTransport(
400+
options: SharedServerTargetOptions,
401+
hasUrl: boolean,
402+
hasCommand: boolean,
403+
): TransportType {
404+
const transport = resolveTransportOption(options.transport);
405+
406+
if (!transport) {
407+
if (hasUrl === hasCommand) {
408+
throw usageError("Specify exactly one target: either --url or --command.");
409+
}
410+
411+
return hasUrl ? "http" : "stdio";
412+
}
413+
414+
if (transport === "http") {
415+
if (!hasUrl) {
416+
throw usageError("--transport http requires --url.");
417+
}
418+
if (hasCommand) {
419+
throw usageError("--command can only be used with --transport stdio.");
420+
}
421+
422+
return transport;
423+
}
424+
425+
if (!hasCommand) {
426+
throw usageError("--transport stdio requires --command.");
427+
}
428+
if (hasUrl) {
429+
throw usageError("--url can only be used with --transport http.");
430+
}
431+
432+
return transport;
433+
}
434+
435+
function resolveTransportOption(
436+
value: string | undefined,
437+
): TransportType | undefined {
438+
if (value === undefined) {
439+
return undefined;
440+
}
441+
442+
if (value === "http" || value === "stdio") {
443+
return value;
444+
}
445+
446+
throw usageError(
447+
`Invalid transport "${value}". Use "http" or "stdio".`,
448+
);
449+
}
450+
380451
export function resolveHttpAccessToken(
381452
options: Pick<SharedServerTargetOptions, "accessToken" | "oauthAccessToken">,
382453
): string | undefined {

0 commit comments

Comments
 (0)