bytedance/deer-flow · error · HTTPException

MCP server '{server_name}' command must be a single executab

Error message

MCP server '{server_name}' command must be a single executable name; put parameters in args instead.

What it means

400 raised when the stdio command field is not a single bare executable name: it contains leading/trailing whitespace, path separators ('/' or '\\'), internal whitespace, or shell metacharacters. The API executes the command directly without a shell, so only one token is accepted; parameters belong in args.

Source

Thrown at backend/app/gateway/routers/mcp.py:505

    base = set(_DEFAULT_MCP_STDIO_COMMAND_ALLOWLIST)
    if raw is None:
        return base
    extra = {item.strip() for item in raw.split(",") if item.strip()}
    return base | extra


def _stdio_command_name(command: str | None, *, server_name: str) -> str:
    """Normalize and validate a stdio command field from the API boundary."""
    if command is None or not command.strip():
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=f"MCP server '{server_name}' with stdio transport requires a command.",
        )

    stripped = command.strip()
    has_path_separator = "/" in stripped or "\\" in stripped
    if stripped != command or has_path_separator or any(ch.isspace() for ch in stripped) or any(ch in stripped for ch in _SHELL_METACHARS):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=(f"MCP server '{server_name}' command must be a single executable name; put parameters in args instead."),
        )

    return stripped


def _launcher_option_region(args: list[str], *, grammar: _LauncherGrammar) -> list[str]:
    """Return the leading args a package launcher parses as its own options.

    The region ends at a bare ``--`` or at the package name -- the first token
    that is neither a flag nor the value of one. A ``--flag=value`` token
    carries its own value and never consumes the next one.

    Arity is looked up case-sensitively, because a launcher's short options are:
    npm reads ``-c`` as ``--call`` but ``-C`` as ``--prefix``, which takes a
    value.
    """

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Put only the executable name in command and move all flags into the args array
  2. Use a launcher (npx/uvx/uv run/etc.) from the allowlist as the command and the real package as the first arg
  3. If you need a specific binary path, symlink or install it on PATH and reference it by bare name, or extend the allowlist env var

Example fix

// before
{"command": "uvx --from git+https://... mcp-server"}
// after
{"command": "uvx", "args": ["--from", "git+https://...", "mcp-server"]}
Defensive patterns

Strategy: validation

Validate before calling

const SHELL_METACHARS = new Set(';|&$`><\\"\'(){}[]!*?~');
function assertSingleExecutable(cmd: string) { const s = cmd.trim(); if (/[/\\]/.test(s) || /\s/.test(s) || [...s].some(c => SHELL_METACHARS.has(c)) || s !== cmd) throw new Error('command must be one bare executable name; put parameters in args'); }

Type guard

function isBareExecutableName(cmd: string): boolean { return /^[A-Za-z0-9._-]+$/.test(cmd); }

Try / catch

null

Prevention

When it happens

Trigger: Submitting command: 'npx -y foo' (flags inside command), command: '/usr/local/bin/npx' (absolute path), command: 'npx; rm -rf /' or 'npx && foo' (metacharacters), or command with stray whitespace.

Common situations: Copy-pasting a full CLI invocation from documentation into the command field; hardening-driven rejection of path-qualified binaries; attempts to chain commands or use shell syntax in a field that is exec'd directly.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/1a960babce450423. Report an issue: GitHub.