From 08b6fe08b029cd17cd3aab5cd70a1d30446a5acc Mon Sep 17 00:00:00 2001 From: Gaurav Pandey <112387553+gaurav-pandey-zocdoc@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:51:11 +0530 Subject: [PATCH] fix(mcp): log how many scopes were dropped instead of their names The dropped scope names come straight from the client request, which CodeQL flags as log injection. Logging the count still shows the cap kicked in for a server without putting request data in the log Generated with AI Co-Authored-By: Claude Code --- .../mcp_server/discoverable_endpoints.py | 4 ++-- .../mcp_server/test_discoverable_endpoints.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 72ad3d4b24e..436043ffbbb 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -901,9 +901,9 @@ def _cap_requested_scopes(requested: str | None, server: MCPServer) -> str | Non dropped: Final = [scope for scope in scopes if scope not in configured] if dropped: verbose_logger.debug( - "MCP server %s: dropped requested OAuth scopes %s outside the configured scopes", + "MCP server %s: dropped %d requested OAuth scopes outside the configured scopes", server.server_id, - dropped, + len(dropped), ) return " ".join(scope for scope in scopes if scope in configured) or None diff --git a/tests/unit/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/unit/proxy/_experimental/mcp_server/test_discoverable_endpoints.py index 2137a434e42..f7a7b1d007d 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_discoverable_endpoints.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_discoverable_endpoints.py @@ -5170,17 +5170,22 @@ async def test_authorize_with_enforced_identity_binding_adds_only_openid_beyond_ @pytest.mark.asyncio -async def test_authorize_logs_dropped_scope_names_and_server_but_not_client_credentials(caplog): +@pytest.mark.parametrize( + "client_scope, dropped_count", [("all ir.incidents:read", 1), ("all admin ir.incidents:read", 2)] +) +async def test_authorize_logs_dropped_scope_count_and_server_but_not_client_credentials( + caplog, client_scope, dropped_count +): import logging server = _resource_server(scopes=list(_READ_SCOPES), configured_scopes=_READ_SCOPES) with caplog.at_level(logging.DEBUG, logger="LiteLLM"): - await _upstream_authorize_scope(server, "all ir.incidents:read") + await _upstream_authorize_scope(server, client_scope) dropped_logs = [record for record in caplog.records if "outside the configured" in record.getMessage()] assert [record.levelno for record in dropped_logs] == [logging.DEBUG], caplog.text message = dropped_logs[0].getMessage() - assert "'all'" in message and "res-srv" in message + assert f"dropped {dropped_count} requested" in message and "res-srv" in message assert "ir.incidents:read" not in message assert "gateway-client" not in message and "gateway-secret" not in message