mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(mcp): discover missing OAuth scopes and token_url when authorization_url is set manually
This commit is contained in:
parent
9cca6c3ef1
commit
66f012a06b
2 changed files with 62 additions and 3 deletions
|
|
@ -1447,8 +1447,9 @@ class MCPServerManager:
|
|||
|
||||
auth_type = cast(MCPAuthType, mcp_server.auth_type)
|
||||
server_url = mcp_server.url
|
||||
has_all_upstream_oauth_fields = bool(mcp_server.authorization_url and mcp_server.token_url and scopes)
|
||||
needs_discovery = bool(server_url) and (
|
||||
(auth_type in _UPSTREAM_OAUTH_DISCOVERY_AUTH_TYPES and not mcp_server.authorization_url)
|
||||
(auth_type in _UPSTREAM_OAUTH_DISCOVERY_AUTH_TYPES and not has_all_upstream_oauth_fields)
|
||||
or self._obo_needs_endpoint_discovery(
|
||||
auth_type,
|
||||
mcp_server.token_exchange_endpoint
|
||||
|
|
@ -1467,7 +1468,7 @@ class MCPServerManager:
|
|||
if needs_discovery and mcp_oauth_metadata is None:
|
||||
verbose_logger.warning(
|
||||
"MCP OAuth discovery yielded no metadata for server %s (%s); "
|
||||
"OAuth endpoints stay unresolved until a rebuild succeeds",
|
||||
"OAuth endpoints/scopes stay unresolved until a rebuild succeeds",
|
||||
mcp_server.server_id,
|
||||
server_url,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1026,7 +1026,6 @@ class TestMCPServerManager:
|
|||
"""The gateway's relayed authorize flow (used by the browser-only Authorize) needs the
|
||||
upstream's authorization_url on the registry entry, and these rows never persist one, so
|
||||
the DB build must discover it the same way oauth2 rows do."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
manager = MCPServerManager()
|
||||
row = LiteLLM_MCPServerTable(
|
||||
|
|
@ -1053,6 +1052,65 @@ class TestMCPServerManager:
|
|||
assert built.authorization_url == "https://idp.example.com/authorize"
|
||||
assert built.token_url == "https://idp.example.com/token"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_from_table_discovers_scopes_when_authorization_url_is_manual(self):
|
||||
"""An admin-typed authorization_url must not switch off discovery for the fields left
|
||||
blank: without the scopes_supported backfill the authorize redirect goes out scope-less
|
||||
and IdPs like Google hard-fail it with 400 "Missing required parameter: scope"."""
|
||||
manager = MCPServerManager()
|
||||
row = LiteLLM_MCPServerTable(
|
||||
server_id="manual-auth-url-1",
|
||||
alias="manual_auth_url",
|
||||
description="manual authorization_url, blank scopes",
|
||||
url="https://up.example.com/mcp",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
authorization_url="https://idp.example.com/manual-authorize",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
metadata = MCPOAuthMetadata(
|
||||
authorization_url="https://idp.example.com/discovered-authorize",
|
||||
token_url="https://idp.example.com/token",
|
||||
registration_url=None,
|
||||
scopes=["calendar.read", "calendar.write"],
|
||||
)
|
||||
with patch.object(manager, "_descovery_metadata", new=AsyncMock(return_value=metadata)) as mock_discovery:
|
||||
built = await manager.build_mcp_server_from_table(row, credentials_are_encrypted=False)
|
||||
|
||||
mock_discovery.assert_awaited_once()
|
||||
assert built.authorization_url == "https://idp.example.com/manual-authorize"
|
||||
assert built.token_url == "https://idp.example.com/token"
|
||||
assert built.scopes == ["calendar.read", "calendar.write"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_from_table_skips_discovery_when_all_upstream_oauth_fields_present(self):
|
||||
"""A fully hand-configured server (authorization_url, token_url, and scopes all set) has
|
||||
nothing left for discovery to fill, so the build must not fetch upstream metadata."""
|
||||
manager = MCPServerManager()
|
||||
row = LiteLLM_MCPServerTable(
|
||||
server_id="fully-manual-1",
|
||||
alias="fully_manual",
|
||||
description="all upstream oauth fields set by the admin",
|
||||
url="https://up.example.com/mcp",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
authorization_url="https://idp.example.com/manual-authorize",
|
||||
token_url="https://idp.example.com/manual-token",
|
||||
credentials={"scopes": ["calendar.read"]},
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
)
|
||||
|
||||
with patch.object(manager, "_descovery_metadata", new=AsyncMock(return_value=None)) as mock_discovery:
|
||||
built = await manager.build_mcp_server_from_table(row, credentials_are_encrypted=False)
|
||||
|
||||
mock_discovery.assert_not_awaited()
|
||||
assert built.authorization_url == "https://idp.example.com/manual-authorize"
|
||||
assert built.token_url == "https://idp.example.com/manual-token"
|
||||
assert built.scopes == ["calendar.read"]
|
||||
|
||||
async def _capture_subject_token(self, call) -> Optional[str]:
|
||||
"""Run a manager method (via ``call(manager)``) and return the subject_token it threaded
|
||||
into ``_create_mcp_client``."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue