{"record":{"id":"83104c5c6ef0eb4c","repo":"odysseus-dev/odysseus","slug":"command-is-required-for-stdio-transport","errorCode":null,"errorMessage":"command is required for stdio transport","messagePattern":"command is required for stdio transport","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/mcp/mcp_routes.py","lineNumber":178,"sourceCode":"        request: Request,\n        name: str = Form(...),\n        transport: str = Form(\"stdio\"),\n        command: str = Form(None),\n        args: str = Form(\"[]\"),\n        env: str = Form(\"{}\"),\n        url: str = Form(None),\n        oauth_file: str = Form(None),\n        oauth_config: str = Form(None),\n    ):\n        \"\"\"Add a new MCP server config and attempt connection. Admin-only:\n        registering a stdio server is equivalent to executing arbitrary\n        binaries on the host.\"\"\"\n        require_admin(request)\n        server_id = str(uuid.uuid4())[:8]\n\n        # Validate\n        if transport == \"stdio\" and not command:\n            raise HTTPException(400, \"command is required for stdio transport\")\n        if transport == \"sse\" and not url:\n            raise HTTPException(400, \"url is required for SSE transport\")\n        if transport == \"http\" and not url:\n            raise HTTPException(400, \"url is required for HTTP transport\")\n\n        # Parse JSON fields\n        try:\n            parsed_args = json.loads(args) if args else []\n        except json.JSONDecodeError:\n            parsed_args = []\n        try:\n            parsed_env = json.loads(env) if env else {}\n        except json.JSONDecodeError:\n            parsed_env = {}\n        if not isinstance(parsed_env, dict):\n            parsed_env = {}\n\n        # Parse OAuth config","sourceCodeStart":160,"sourceCodeEnd":196,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/mcp/mcp_routes.py#L160-L196","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Send command (e.g. command=\"npx\") as a multipart/urlencoded form field together with transport=\"stdio\".","Confirm you are using the correct content type — the endpoint reads Form(...) fields, so a JSON body will leave command empty.","If you actually meant a remote server, use transport=\"sse\" or \"http\" with a url instead."],"exampleFix":"# before (JSON body → Form endpoint, command lost)\nrequests.post(url, json={\"transport\": \"stdio\", \"command\": \"npx\"})\n\n# after\nrequests.post(url, data={\"transport\": \"stdio\", \"command\": \"npx\", \"args\": '[\"-y\",\"some-mcp\"]'})","handlingStrategy":"validation","validationCode":"def stdio_config_valid(data: dict) -> bool:\n    return data.get(\"transport\") != \"stdio\" or bool(data.get(\"command\"))","typeGuard":null,"tryCatchPattern":"Catch the 400 and surface 'command required' to the user instead of retrying; the server state is unchanged.","preventionTips":["Send form data (not JSON) to Form()-based endpoints.","Build a small client wrapper that asserts transport→required-field pairs before posting."],"tags":["mcp","validation","form-data","stdio"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}