{"record":{"id":"cec5147b0f5cfacd","repo":"odysseus-dev/odysseus","slug":"url-is-required-for-http-transport","errorCode":null,"errorMessage":"url is required for HTTP transport","messagePattern":"url is required for HTTP transport","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/mcp/mcp_routes.py","lineNumber":182,"sourceCode":"        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\n        parsed_oauth_config = None\n        if oauth_config:\n            try:\n                parsed_oauth_config = _sanitize_mcp_oauth_config(json.loads(oauth_config))","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/mcp/mcp_routes.py#L164-L200","documentation":"Same registration endpoint as the SSE check: transport=\"http\" (streamable HTTP transport) requires a url. The validation runs after require_admin, so it also implies you are authenticated as an admin; a missing url yields 400 before the server record is created.","triggerScenarios":"POST with transport=\"http\" and no url form field; url present but empty string; sending the url in a JSON body while the endpoint expects Form fields.","commonSituations":"Newer streamable-HTTP MCP server registered with the wrong field name; client switched transport dropdown to http without filling the endpoint; env-specific URL not templated into the request.","solutions":["Send url as a form field, e.g. url=\"https://mcp.example.com/mcp\" with transport=\"http\".","Double-check the form field name is exactly 'url' and the request is multipart/form-data or application/x-www-form-urlencoded.","Test the URL with curl first — a reachable endpoint avoids a follow-up connect failure after registration."],"exampleFix":"# before\ndata = {\"transport\": \"http\"}\n\n# after\ndata = {\"transport\": \"http\", \"url\": \"https://mcp.example.com/mcp\"}","handlingStrategy":"validation","validationCode":"def http_config_valid(data: dict) -> bool:\n    return data.get(\"transport\") != \"http\" or bool(data.get(\"url\"))","typeGuard":null,"tryCatchPattern":"Catch the 400 and correct the form payload; the endpoint is transactional — no partial server is created.","preventionTips":["Use one shared request-builder that enforces per-transport required fields.","Smoke-test the URL's reachability before registering."],"tags":["mcp","validation","http-transport","url"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}