From 298936f83d8e2775e72ff455bf2241e937467f70 Mon Sep 17 00:00:00 2001 From: "Claude (greptile fixer)" Date: Wed, 20 May 2026 15:31:57 +0000 Subject: [PATCH] fix(mcp,auth): address greptile review concerns - handle_sse_mcp now calls _raise_preemptive_401_for_unauthenticated_servers so SSE clients to pass-through OAuth MCP servers receive the RFC 9728 401 + WWW-Authenticate challenge that the streamable-HTTP path already emits. - get_request_route strips a trailing slash from root_path before length-based prefix removal so non-canonical ASGI root_path values like "/litellm/" don't strip the leading slash from the returned route. - _mcp_oauth_user_api_key_auth's cookie JWT decode now passes options={"verify_aud": False} so a future revision of the UI session JWT containing an aud claim cannot silently downgrade the request to unauthenticated. Co-authored-by: Claude --- litellm/proxy/_experimental/mcp_server/server.py | 9 +++++++++ litellm/proxy/auth/auth_utils.py | 13 ++++++++----- .../mcp_management_endpoints.py | 1 + 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/server.py b/litellm/proxy/_experimental/mcp_server/server.py index a8b98b3e2e8..b4b9a2d9aad 100644 --- a/litellm/proxy/_experimental/mcp_server/server.py +++ b/litellm/proxy/_experimental/mcp_server/server.py @@ -3025,6 +3025,15 @@ if MCP_AVAILABLE: verbose_logger.debug( f"MCP server auth headers: {list(mcp_server_auth_headers.keys()) if mcp_server_auth_headers else None}" ) + # https://datatracker.ietf.org/doc/html/rfc9728#name-www-authenticate-response + await _raise_preemptive_401_for_unauthenticated_servers( + scope=scope, + mcp_servers=mcp_servers, + oauth2_headers=oauth2_headers, + mcp_server_auth_headers=mcp_server_auth_headers, + user_api_key_auth=user_api_key_auth, + client_ip=_sse_client_ip, + ) set_auth_context( user_api_key_auth=user_api_key_auth, mcp_auth_header=mcp_auth_header, diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index ee86e923924..3c8fd30e7df 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -506,13 +506,16 @@ def get_request_route(request: Request) -> str: if not isinstance(scope, dict): return str(request.url.path) raw_path: str = str(scope.get("path", request.url.path)) - root_path: str = str(scope.get("app_root_path", scope.get("root_path", ""))) + root_path: str = str( + scope.get("app_root_path", scope.get("root_path", "")) + ).rstrip("/") if not isinstance(raw_path, str): return str(request.url.path) - # Only strip root_path when it is a meaningful prefix (not bare "/"). - # Stripping bare "/" would remove the leading slash from every path - # e.g. "/team/new" → "team/new", breaking route matching. - if root_path and root_path != "/" and raw_path.startswith(root_path): + # Only strip root_path when it is a meaningful prefix. Trailing + # slashes are stripped above so the result always keeps its leading + # "/" — stripping a bare "/" or "/prefix/" would otherwise produce + # paths like "team/new" and break route matching. + if root_path and raw_path.startswith(root_path): return raw_path[len(root_path) :] return raw_path except Exception as e: diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index b2e21768842..0ffc3f7d2ab 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -1522,6 +1522,7 @@ if MCP_AVAILABLE: token_cookie, master_key, algorithms=["HS256"], + options={"verify_aud": False}, ) if decoded.get("login_method") in ("sso", "username_password"): cookie_key = decoded.get("key", "")