odysseus-dev/odysseus · error · HTTPException

command is required for stdio transport

Error message

command is required for stdio transport

What it means

Admin-only POST that registers an MCP server rejects a stdio transport config with no command. A stdio server is spawned by executing a local binary, so command is the minimum required field; without it FastAPI returns 400 before any connection attempt.

Source

Thrown at routes/mcp/mcp_routes.py:178

        request: Request,
        name: str = Form(...),
        transport: str = Form("stdio"),
        command: str = Form(None),
        args: str = Form("[]"),
        env: str = Form("{}"),
        url: str = Form(None),
        oauth_file: str = Form(None),
        oauth_config: str = Form(None),
    ):
        """Add a new MCP server config and attempt connection. Admin-only:
        registering a stdio server is equivalent to executing arbitrary
        binaries on the host."""
        require_admin(request)
        server_id = str(uuid.uuid4())[:8]

        # Validate
        if transport == "stdio" and not command:
            raise HTTPException(400, "command is required for stdio transport")
        if transport == "sse" and not url:
            raise HTTPException(400, "url is required for SSE transport")
        if transport == "http" and not url:
            raise HTTPException(400, "url is required for HTTP transport")

        # Parse JSON fields
        try:
            parsed_args = json.loads(args) if args else []
        except json.JSONDecodeError:
            parsed_args = []
        try:
            parsed_env = json.loads(env) if env else {}
        except json.JSONDecodeError:
            parsed_env = {}
        if not isinstance(parsed_env, dict):
            parsed_env = {}

        # Parse OAuth config

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Send command (e.g. command="npx") as a multipart/urlencoded form field together with transport="stdio".
  2. Confirm you are using the correct content type — the endpoint reads Form(...) fields, so a JSON body will leave command empty.
  3. If you actually meant a remote server, use transport="sse" or "http" with a url instead.

Example fix

# before (JSON body → Form endpoint, command lost)
requests.post(url, json={"transport": "stdio", "command": "npx"})

# after
requests.post(url, data={"transport": "stdio", "command": "npx", "args": '["-y","some-mcp"]'})
Defensive patterns

Strategy: validation

Validate before calling

def stdio_config_valid(data: dict) -> bool:
    return data.get("transport") != "stdio" or bool(data.get("command"))

Try / catch

Catch the 400 and surface 'command required' to the user instead of retrying; the server state is unchanged.

Prevention

When it happens

Trigger: POST to the MCP servers endpoint with transport="stdio" but command omitted or empty (Form default None), e.g. when the client sends JSON fields but the endpoint expects form data so command never arrives.

Common situations: Content-type mismatch: sending JSON body to a Form()-based endpoint so command is silently None; UI dialog where the command input was left blank; migrating a config from SSE/HTTP to stdio and forgetting the command field.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/83104c5c6ef0eb4c. Report an issue: GitHub.