mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(mcp): route delegate_auth_to_upstream servers to upstream OAuth discovery
The oauth-protected-resource metadata endpoint only checked is_oauth_passthrough when deciding whether to proxy the upstream's discovery metadata. Servers with auth_type=oauth2 and delegate_auth_to_upstream=True fell through to the default gateway metadata, so MCP clients discovered the gateway as the authorization server instead of the upstream IdP. This broke the PKCE flow: the gateway intercepted the OAuth callback instead of letting the client receive the authorization code directly. Three changes: 1. _build_oauth_protected_resource_response now also tries to fetch upstream resource metadata for delegates_interactive_oauth_to_upstream servers, with a graceful fallback to gateway-served AS metadata when the upstream does not expose the well-known endpoint (unlike is_oauth_passthrough which 502s). 2. _build_oauth_authorization_server_response returns the upstream's authorization_url and token_url for delegate_auth_to_upstream servers so the client authenticates directly with the upstream IdP. 3. register_client_with_server passes through the client's redirect_uris for delegate_auth_to_upstream servers instead of substituting the gateway's callback URL. Adds MCPServer.delegates_interactive_oauth_to_upstream property to encapsulate the repeated auth_type + delegate_auth_to_upstream + not client_credentials check. Closes LIT-4120 Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
70eb4e5d00
commit
0e0dc9871d
3 changed files with 450 additions and 25 deletions
|
|
@ -581,12 +581,19 @@ async def register_client_with_server(
|
|||
response_types: Optional[list],
|
||||
token_endpoint_auth_method: Optional[str],
|
||||
fallback_client_id: Optional[str] = None,
|
||||
client_redirect_uris: Optional[list] = None,
|
||||
):
|
||||
request_base_url = get_request_base_url(request)
|
||||
|
||||
_is_delegated = mcp_server.delegates_interactive_oauth_to_upstream
|
||||
effective_redirect_uris = (
|
||||
client_redirect_uris if _is_delegated and client_redirect_uris else [f"{request_base_url}/callback"]
|
||||
)
|
||||
|
||||
dummy_return = {
|
||||
"client_id": fallback_client_id or mcp_server.server_name,
|
||||
"client_secret": "dummy",
|
||||
"redirect_uris": [f"{request_base_url}/callback"],
|
||||
"redirect_uris": effective_redirect_uris,
|
||||
}
|
||||
|
||||
if mcp_server.client_id and mcp_server.client_secret:
|
||||
|
|
@ -600,7 +607,7 @@ async def register_client_with_server(
|
|||
|
||||
register_data = {
|
||||
"client_name": client_name,
|
||||
"redirect_uris": [f"{request_base_url}/callback"],
|
||||
"redirect_uris": effective_redirect_uris,
|
||||
"grant_types": grant_types or [],
|
||||
"response_types": response_types or [],
|
||||
"token_endpoint_auth_method": token_endpoint_auth_method or "",
|
||||
|
|
@ -1030,38 +1037,61 @@ async def _build_oauth_protected_resource_response(
|
|||
else:
|
||||
resource_url = f"{request_base_url}/mcp"
|
||||
|
||||
# Pass-through branch: proxy the upstream's own metadata so discovery
|
||||
# directs the client at the real IdP (Okta, Keycloak, …) instead of us.
|
||||
if mcp_server is not None and mcp_server.is_oauth_passthrough:
|
||||
_delegates_to_upstream = mcp_server is not None and (
|
||||
mcp_server.is_oauth_passthrough or mcp_server.delegates_interactive_oauth_to_upstream
|
||||
)
|
||||
|
||||
if _delegates_to_upstream:
|
||||
assert mcp_server is not None # narrowing for type checker
|
||||
upstream_metadata: Optional[dict] = None
|
||||
fetch_failed = False
|
||||
try:
|
||||
upstream_metadata = await fetch_upstream_oauth_protected_resource(mcp_server)
|
||||
except Exception as exc:
|
||||
verbose_logger.warning(
|
||||
"Failed to fetch upstream oauth-protected-resource metadata "
|
||||
f"for pass-through MCP server {mcp_server.name!r}: {exc}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=(
|
||||
f"Failed to fetch upstream oauth-protected-resource metadata for MCP server {mcp_server.name!r}"
|
||||
),
|
||||
f"for MCP server {mcp_server.name!r}: {exc}"
|
||||
)
|
||||
if mcp_server.is_oauth_passthrough:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=(
|
||||
"Failed to fetch upstream oauth-protected-resource metadata "
|
||||
f"for MCP server {mcp_server.name!r}"
|
||||
),
|
||||
)
|
||||
fetch_failed = True
|
||||
|
||||
if upstream_metadata is not None:
|
||||
response = {**upstream_metadata, "resource": resource_url}
|
||||
return response
|
||||
|
||||
# Upstream responded but with non-200 or non-dict payload. For
|
||||
# pass-through servers the gateway is NOT the authorization server,
|
||||
# so we must not fall through to the default gateway metadata —
|
||||
# that would point clients at the wrong IdP.
|
||||
verbose_logger.warning(
|
||||
f"Upstream oauth-protected-resource metadata unavailable for pass-through MCP server {mcp_server.name!r}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=(f"Upstream oauth-protected-resource metadata unavailable for MCP server {mcp_server.name!r}"),
|
||||
)
|
||||
if mcp_server.is_oauth_passthrough:
|
||||
verbose_logger.warning(
|
||||
"Upstream oauth-protected-resource metadata unavailable "
|
||||
f"for pass-through MCP server {mcp_server.name!r}"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=(
|
||||
"Upstream oauth-protected-resource metadata unavailable "
|
||||
f"for MCP server {mcp_server.name!r}"
|
||||
),
|
||||
)
|
||||
|
||||
if fetch_failed:
|
||||
verbose_logger.info(
|
||||
"delegate-auth MCP server %r: upstream resource metadata "
|
||||
"unavailable, falling back to gateway-served AS metadata",
|
||||
mcp_server.name,
|
||||
)
|
||||
return {
|
||||
"authorization_servers": [
|
||||
(f"{request_base_url}/{mcp_server_name}" if mcp_server_name else f"{request_base_url}")
|
||||
],
|
||||
"resource": resource_url,
|
||||
"scopes_supported": (mcp_server.scopes if mcp_server and mcp_server.scopes else []),
|
||||
}
|
||||
|
||||
return {
|
||||
"authorization_servers": [
|
||||
|
|
@ -1149,8 +1179,32 @@ def _build_oauth_authorization_server_response(
|
|||
if mcp_server_name:
|
||||
mcp_server = global_mcp_server_manager.get_mcp_server_by_name(mcp_server_name, client_ip=client_ip)
|
||||
|
||||
if mcp_server is not None and mcp_server.delegates_interactive_oauth_to_upstream:
|
||||
if not mcp_server.authorization_url or not mcp_server.token_url:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=(
|
||||
f"MCP server {mcp_server.name!r} has delegate_auth_to_upstream "
|
||||
"but missing authorization_url or token_url"
|
||||
),
|
||||
)
|
||||
registration_endpoint = mcp_server.registration_url or (
|
||||
f"{request_base_url}/{mcp_server_name}/register" if mcp_server_name else f"{request_base_url}/register"
|
||||
)
|
||||
return {
|
||||
"issuer": request_base_url,
|
||||
"authorization_endpoint": mcp_server.authorization_url,
|
||||
"token_endpoint": mcp_server.token_url,
|
||||
"response_types_supported": ["code"],
|
||||
"scopes_supported": (mcp_server.scopes if mcp_server.scopes else []),
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
"token_endpoint_auth_methods_supported": ["none"],
|
||||
"registration_endpoint": registration_endpoint,
|
||||
}
|
||||
|
||||
return {
|
||||
"issuer": request_base_url, # point to your proxy
|
||||
"issuer": request_base_url,
|
||||
"authorization_endpoint": authorization_endpoint,
|
||||
"token_endpoint": token_endpoint,
|
||||
"response_types_supported": ["code"],
|
||||
|
|
@ -1158,7 +1212,6 @@ def _build_oauth_authorization_server_response(
|
|||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
"token_endpoint_auth_methods_supported": ["client_secret_post"],
|
||||
# Claude expects a registration endpoint, even if we just fake it
|
||||
"registration_endpoint": (
|
||||
f"{request_base_url}/{mcp_server_name}/register" if mcp_server_name else f"{request_base_url}/register"
|
||||
),
|
||||
|
|
@ -1288,6 +1341,7 @@ async def register_client(request: Request, mcp_server_name: Optional[str] = Non
|
|||
"client_secret": "dummy",
|
||||
"redirect_uris": [f"{request_base_url}/callback"],
|
||||
}
|
||||
client_redirect_uris = data.get("redirect_uris")
|
||||
client_ip = IPAddressUtils.get_mcp_client_ip(request)
|
||||
if not mcp_server_name:
|
||||
resolved = _resolve_oauth2_server_for_root_endpoints(client_ip=client_ip)
|
||||
|
|
@ -1300,6 +1354,7 @@ async def register_client(request: Request, mcp_server_name: Optional[str] = Non
|
|||
response_types=data.get("response_types", []),
|
||||
token_endpoint_auth_method=data.get("token_endpoint_auth_method", ""),
|
||||
fallback_client_id=resolved.server_name or resolved.name,
|
||||
client_redirect_uris=client_redirect_uris,
|
||||
)
|
||||
return dummy_return
|
||||
|
||||
|
|
@ -1314,4 +1369,5 @@ async def register_client(request: Request, mcp_server_name: Optional[str] = Non
|
|||
response_types=data.get("response_types", []),
|
||||
token_endpoint_auth_method=data.get("token_endpoint_auth_method", ""),
|
||||
fallback_client_id=mcp_server_name,
|
||||
client_redirect_uris=client_redirect_uris,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -203,6 +203,21 @@ class MCPServer(BaseModel):
|
|||
return False
|
||||
return any(h.lower() == "authorization" for h in self.extra_headers)
|
||||
|
||||
@property
|
||||
def delegates_interactive_oauth_to_upstream(self) -> bool:
|
||||
"""True when the gateway should point MCP clients at the upstream IdP
|
||||
for OAuth discovery rather than acting as an OAuth intermediary.
|
||||
|
||||
Requires ``auth_type=oauth2``, ``delegate_auth_to_upstream=True``, and
|
||||
NOT ``client_credentials`` (M2M servers use stored creds; exposing
|
||||
them anonymously would be a privilege escalation).
|
||||
"""
|
||||
return (
|
||||
self.auth_type == MCPAuth.oauth2
|
||||
and self.delegate_auth_to_upstream is True
|
||||
and not self.has_client_credentials
|
||||
)
|
||||
|
||||
@property
|
||||
def has_token_exchange_config(self) -> bool:
|
||||
"""True if this server is configured for OAuth2 token exchange (OBO / RFC 8693)."""
|
||||
|
|
|
|||
|
|
@ -3011,3 +3011,357 @@ async def test_token_endpoint_client_secret_basic_without_secret_returns_400():
|
|||
code_verifier="verifier",
|
||||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Tests for delegate_auth_to_upstream OAuth discovery (LIT-4120)
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
|
||||
def _create_delegate_auth_server(
|
||||
server_id="delegate_server",
|
||||
name="delegate_mcp",
|
||||
server_name="delegate_mcp",
|
||||
url="https://upstream-mcp.example.com/mcp",
|
||||
authorization_url="https://upstream-idp.example.com/authorize",
|
||||
token_url="https://upstream-idp.example.com/token",
|
||||
registration_url=None,
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
):
|
||||
from litellm.proxy._types import MCPTransport
|
||||
from litellm.types.mcp import MCPAuth
|
||||
from litellm.types.mcp_server.mcp_server_manager import MCPServer
|
||||
|
||||
return MCPServer(
|
||||
server_id=server_id,
|
||||
name=name,
|
||||
server_name=server_name,
|
||||
url=url,
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
delegate_auth_to_upstream=True,
|
||||
authorization_url=authorization_url,
|
||||
token_url=token_url,
|
||||
registration_url=registration_url,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
scopes=["read"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delegate_auth_resource_metadata_proxies_upstream():
|
||||
"""When upstream has oauth-protected-resource metadata, the gateway
|
||||
should proxy it (with resource rewritten) for delegate_auth_to_upstream
|
||||
servers, just like is_oauth_passthrough servers."""
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
_build_oauth_protected_resource_response,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
server = _create_delegate_auth_server()
|
||||
global_mcp_server_manager.registry[server.server_id] = server
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
upstream_metadata = {
|
||||
"authorization_servers": ["https://upstream-idp.example.com"],
|
||||
"resource": "https://upstream-mcp.example.com/mcp",
|
||||
"scopes_supported": ["read", "write"],
|
||||
}
|
||||
|
||||
try:
|
||||
with patch(
|
||||
"litellm.proxy._experimental.mcp_server.discoverable_endpoints.fetch_upstream_oauth_protected_resource",
|
||||
new_callable=AsyncMock,
|
||||
return_value=upstream_metadata,
|
||||
):
|
||||
response = await _build_oauth_protected_resource_response(
|
||||
request=mock_request,
|
||||
mcp_server_name="delegate_mcp",
|
||||
use_standard_pattern=True,
|
||||
)
|
||||
assert response["authorization_servers"] == ["https://upstream-idp.example.com"]
|
||||
assert response["resource"] == "https://gateway.example.com/mcp/delegate_mcp"
|
||||
assert response["scopes_supported"] == ["read", "write"]
|
||||
finally:
|
||||
global_mcp_server_manager.registry.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delegate_auth_resource_metadata_fallback_to_gateway():
|
||||
"""When upstream has no oauth-protected-resource metadata, the gateway
|
||||
should serve its own resource metadata (with authorization_servers
|
||||
pointing at the gateway) for delegate_auth_to_upstream servers.
|
||||
The AS metadata endpoint will return the upstream's endpoints."""
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
_build_oauth_protected_resource_response,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
server = _create_delegate_auth_server()
|
||||
global_mcp_server_manager.registry[server.server_id] = server
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
try:
|
||||
with patch(
|
||||
"litellm.proxy._experimental.mcp_server.discoverable_endpoints.fetch_upstream_oauth_protected_resource",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
):
|
||||
response = await _build_oauth_protected_resource_response(
|
||||
request=mock_request,
|
||||
mcp_server_name="delegate_mcp",
|
||||
use_standard_pattern=True,
|
||||
)
|
||||
assert response["authorization_servers"] == ["https://gateway.example.com/delegate_mcp"]
|
||||
assert response["resource"] == "https://gateway.example.com/mcp/delegate_mcp"
|
||||
finally:
|
||||
global_mcp_server_manager.registry.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delegate_auth_resource_metadata_fetch_error_falls_back():
|
||||
"""When fetching upstream metadata fails for a delegate_auth_to_upstream
|
||||
server, fall back to gateway metadata (unlike passthrough which 502s)."""
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
_build_oauth_protected_resource_response,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
server = _create_delegate_auth_server()
|
||||
global_mcp_server_manager.registry[server.server_id] = server
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
try:
|
||||
with patch(
|
||||
"litellm.proxy._experimental.mcp_server.discoverable_endpoints.fetch_upstream_oauth_protected_resource",
|
||||
new_callable=AsyncMock,
|
||||
side_effect=Exception("connection refused"),
|
||||
):
|
||||
response = await _build_oauth_protected_resource_response(
|
||||
request=mock_request,
|
||||
mcp_server_name="delegate_mcp",
|
||||
use_standard_pattern=True,
|
||||
)
|
||||
assert response["authorization_servers"] == ["https://gateway.example.com/delegate_mcp"]
|
||||
finally:
|
||||
global_mcp_server_manager.registry.clear()
|
||||
|
||||
|
||||
def test_delegate_auth_as_metadata_returns_upstream_endpoints():
|
||||
"""The authorization server metadata for a delegate_auth_to_upstream
|
||||
server should return the upstream's authorization/token endpoints,
|
||||
not the gateway's proxy endpoints."""
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
_build_oauth_authorization_server_response,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
server = _create_delegate_auth_server()
|
||||
global_mcp_server_manager.registry[server.server_id] = server
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
try:
|
||||
response = _build_oauth_authorization_server_response(
|
||||
request=mock_request,
|
||||
mcp_server_name="delegate_mcp",
|
||||
)
|
||||
assert response["authorization_endpoint"] == "https://upstream-idp.example.com/authorize"
|
||||
assert response["token_endpoint"] == "https://upstream-idp.example.com/token"
|
||||
assert response["token_endpoint_auth_methods_supported"] == ["none"]
|
||||
finally:
|
||||
global_mcp_server_manager.registry.clear()
|
||||
|
||||
|
||||
def test_delegate_auth_as_metadata_uses_upstream_registration_url():
|
||||
"""When the delegate_auth_to_upstream server has a registration_url,
|
||||
the AS metadata should return it instead of the gateway's."""
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
_build_oauth_authorization_server_response,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
server = _create_delegate_auth_server(
|
||||
registration_url="https://upstream-idp.example.com/register",
|
||||
)
|
||||
global_mcp_server_manager.registry[server.server_id] = server
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
try:
|
||||
response = _build_oauth_authorization_server_response(
|
||||
request=mock_request,
|
||||
mcp_server_name="delegate_mcp",
|
||||
)
|
||||
assert response["registration_endpoint"] == "https://upstream-idp.example.com/register"
|
||||
finally:
|
||||
global_mcp_server_manager.registry.clear()
|
||||
|
||||
|
||||
def test_delegate_auth_as_metadata_falls_back_to_gateway_register():
|
||||
"""When the delegate_auth_to_upstream server has no registration_url,
|
||||
the AS metadata registration_endpoint should point at the gateway."""
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
_build_oauth_authorization_server_response,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
|
||||
server = _create_delegate_auth_server(registration_url=None)
|
||||
global_mcp_server_manager.registry[server.server_id] = server
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
try:
|
||||
response = _build_oauth_authorization_server_response(
|
||||
request=mock_request,
|
||||
mcp_server_name="delegate_mcp",
|
||||
)
|
||||
assert response["registration_endpoint"] == "https://gateway.example.com/delegate_mcp/register"
|
||||
finally:
|
||||
global_mcp_server_manager.registry.clear()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delegate_auth_register_passes_client_redirect_uris():
|
||||
"""For delegate_auth_to_upstream servers, the registration dummy
|
||||
response should use the client's redirect_uris, not the gateway's."""
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
register_client_with_server,
|
||||
)
|
||||
|
||||
server = _create_delegate_auth_server(
|
||||
client_id="upstream-client",
|
||||
client_secret="upstream-secret",
|
||||
)
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
result = await register_client_with_server(
|
||||
request=mock_request,
|
||||
mcp_server=server,
|
||||
client_name="opencode",
|
||||
grant_types=["authorization_code"],
|
||||
response_types=["code"],
|
||||
token_endpoint_auth_method="none",
|
||||
client_redirect_uris=["http://127.0.0.1:9876/callback"],
|
||||
)
|
||||
assert result["redirect_uris"] == ["http://127.0.0.1:9876/callback"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_delegate_register_uses_gateway_callback():
|
||||
"""For non-delegate servers, the registration response should
|
||||
use the gateway's callback URL as before."""
|
||||
from litellm.proxy._types import MCPTransport
|
||||
from litellm.types.mcp import MCPAuth
|
||||
from litellm.types.mcp_server.mcp_server_manager import MCPServer
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
|
||||
register_client_with_server,
|
||||
)
|
||||
|
||||
server = MCPServer(
|
||||
server_id="normal_oauth",
|
||||
name="normal_oauth",
|
||||
server_name="normal_oauth",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
client_id="client-id",
|
||||
client_secret="client-secret",
|
||||
authorization_url="https://idp.example.com/authorize",
|
||||
token_url="https://idp.example.com/token",
|
||||
)
|
||||
|
||||
mock_request = MagicMock()
|
||||
mock_request.base_url = "https://gateway.example.com/"
|
||||
mock_request.headers = {}
|
||||
|
||||
result = await register_client_with_server(
|
||||
request=mock_request,
|
||||
mcp_server=server,
|
||||
client_name="opencode",
|
||||
grant_types=["authorization_code"],
|
||||
response_types=["code"],
|
||||
token_endpoint_auth_method="none",
|
||||
client_redirect_uris=["http://127.0.0.1:9876/callback"],
|
||||
)
|
||||
assert result["redirect_uris"] == ["https://gateway.example.com/callback"]
|
||||
|
||||
|
||||
def test_delegates_interactive_oauth_to_upstream_property():
|
||||
"""Verify the MCPServer property correctly identifies servers that
|
||||
should delegate OAuth to the upstream."""
|
||||
from litellm.proxy._types import MCPTransport
|
||||
from litellm.types.mcp import MCPAuth
|
||||
from litellm.types.mcp_server.mcp_server_manager import MCPServer
|
||||
|
||||
delegate_server = MCPServer(
|
||||
server_id="s1",
|
||||
name="s1",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
delegate_auth_to_upstream=True,
|
||||
)
|
||||
assert delegate_server.delegates_interactive_oauth_to_upstream is True
|
||||
|
||||
non_delegate_server = MCPServer(
|
||||
server_id="s2",
|
||||
name="s2",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
delegate_auth_to_upstream=False,
|
||||
)
|
||||
assert non_delegate_server.delegates_interactive_oauth_to_upstream is False
|
||||
|
||||
m2m_delegate_server = MCPServer(
|
||||
server_id="s3",
|
||||
name="s3",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.oauth2,
|
||||
delegate_auth_to_upstream=True,
|
||||
oauth2_flow="client_credentials",
|
||||
client_id="cid",
|
||||
client_secret="csec",
|
||||
token_url="https://idp.example.com/token",
|
||||
)
|
||||
assert m2m_delegate_server.delegates_interactive_oauth_to_upstream is False
|
||||
|
||||
none_auth_server = MCPServer(
|
||||
server_id="s4",
|
||||
name="s4",
|
||||
transport=MCPTransport.http,
|
||||
auth_type=MCPAuth.none,
|
||||
delegate_auth_to_upstream=True,
|
||||
)
|
||||
assert none_auth_server.delegates_interactive_oauth_to_upstream is False
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue