cli: an empty flag value is accepted, so mcp-use dev --host= binds every interface and --port= picks a random port
Summary
takeValue in libraries/typescript/packages/cli/src/bin/args.ts (lines 108-117) returns an inline value without checking whether it is empty:
/** Consume the flag's value: inline (`=`) or the next argv token. */
const takeValue = (): string => {
if (inline !== undefined) return inline;
const next = argv[++i];
if (next === undefined || next.startsWith("-")) {
throw new Error(`Missing value for ${flag}`);
}
return next;
};So --host= yields "" and --port= yields "". Neither is treated as a missing value, and both then mean something the user did not ask for.
The Problem
--host=binds every interface instead of loopback.resolveListenHostreturnsexplicitHostwhenever it is notundefined, so""survives:if (explicitHost !== undefined) return explicitHost; const envHost = env.HOST?.trim(); if (envHost !== undefined && envHost !== "") return envHost; return configuredHost ?? DEFAULT_LISTEN_HOST;start.ts:153anddev.ts:361pass the result tolisten, and Node binds""as:::host="" -> {"address":"::","family":"IPv6","port":50666} host=127.0.0.1 -> {"address":"127.0.0.1","family":"IPv4","port":50667}mcp-use dev --host=therefore serves on every interface, while--helpdocuments--host <host> Host to bind (dev/start; default: $HOST or 127.0.0.1)(main.ts:90, and again at 112 and 155). A dev server reachable from the network instead of loopback is a wider exposure than the documented default, with no diagnostic.The env path is already guarded, the flag path is not. Note the asymmetry above:
HOST=""is explicitly skipped viaenvHost !== "", andresolveListenPort'sparsePortreturnsundefinedfor a blank string. Only the flag bypasses these guards, so the same empty value means "fall back to the default" from the environment and "bind everything" from the command line.--port=silently selects an ephemeral port.parsePortinargs.ts:232doesNumber(value), andNumber("")is0, whichisValidPortaccepts because0is legitimately "pick a free port":parseArgs(["start", "--port="]).port // 0 parseArgs(["start", "--port", ""]).port // 0mcp-use start --port=starts on a random port instead of reporting a bad argument. Both the inline and space-separated forms are affected, since thenext.startsWith("-")check does not reject"".Unknown flags throw at line 167, so an accepted flag reasonably reads as an understood one.
Reproduced on main at dcaa0b8. No existing test passes an empty value to any flag; tests/bin-start.test.ts:195 covers --port=8080 only.
For completeness, two adjacent quirks I am not proposing to change, since Number coercion is the documented behaviour and neither is harmful: --port=0x10 yields 16 and --port=1e3 yields 1000.
Proposed fix
Treat a blank value as absent, in the one place both forms pass through:
const takeValue = (): string => {
const value = inline !== undefined ? inline : argv[++i];
if (
value === undefined ||
value.trim() === "" ||
(inline === undefined && value.startsWith("-"))
) {
throw new Error(`Missing value for ${flag}`);
}
return value;
};--host= and --port= then fail with Missing value for --host / Missing value for --port, which is the error this function already raises for a missing value. Non-empty values are unaffected, and the flag path now agrees with the PORT/HOST env handling.
None of the six value-taking flags here (--port, --host, --entry, --path, --mcp-dir, --views-dir) has an "empty clears it" meaning, so nothing depends on the current behaviour. Note this is unlike servers update --root-dir "", where empty deliberately clears a field, but that is a different parser (node:util parseArgs in commands/servers.ts) and is untouched.
Related: #2520 also concerns takeValue, from the other direction (boolean flags accepting a value they discard). Separate defect, and PR #2521 touches the same function, so whichever lands second will need a trivial rebase. Happy to fold this into that PR instead if you would prefer one change.
Source: mcp-use/mcp-use