mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-25 01:02:15 +00:00
fix(mcp): emit absolute resource_metadata URI in fabricated 401 challenge
Per RFC 9728 §3.2 the resource_metadata Bearer challenge must be an absolute URI; strict MCP clients reject relative URIs and fail to initiate discovery. MCPUpstreamAuthError.to_http_exception now accepts the gateway base URL and prepends it when the upstream omitted WWW-Authenticate, and all four call sites (streamable HTTP, SSE, and the two REST tool-list paths) supply it.
This commit is contained in:
parent
20e644748a
commit
ca8e04c05e
4 changed files with 35 additions and 7 deletions
|
|
@ -28,7 +28,7 @@ class MCPUpstreamAuthError(Exception):
|
|||
self.server_name = server_name
|
||||
super().__init__(f"Upstream MCP server {server_name!r} returned {status_code}")
|
||||
|
||||
def to_http_exception(self) -> HTTPException:
|
||||
def to_http_exception(self, base_url: Optional[str] = None) -> HTTPException:
|
||||
"""Convert this upstream-auth error into an ``HTTPException`` that
|
||||
preserves the upstream status code and any ``WWW-Authenticate``
|
||||
challenge, so standards-compliant MCP clients can trigger the
|
||||
|
|
@ -38,13 +38,17 @@ class MCPUpstreamAuthError(Exception):
|
|||
RFC 7235 §3.1) we fabricate a ``Bearer resource_metadata=`` challenge
|
||||
that points at the gateway's standard-pattern well-known endpoint for
|
||||
this server, so MCP clients can still initiate RFC 9728 discovery
|
||||
against the upstream IdP via the gateway's proxied metadata.
|
||||
against the upstream IdP via the gateway's proxied metadata. Callers
|
||||
should pass ``base_url`` (the gateway origin, no trailing slash) so
|
||||
the fabricated URI is absolute as RFC 9728 §3.2 requires; strict
|
||||
clients reject relative URIs in the Bearer challenge.
|
||||
"""
|
||||
challenge: Optional[str] = self.www_authenticate
|
||||
if challenge is None and self.status_code == 401:
|
||||
prefix = base_url.rstrip("/") if base_url else ""
|
||||
challenge = (
|
||||
"Bearer resource_metadata="
|
||||
f'"/.well-known/oauth-protected-resource/mcp/{self.server_name}"'
|
||||
f'"{prefix}/.well-known/oauth-protected-resource/mcp/{self.server_name}"'
|
||||
)
|
||||
return HTTPException(
|
||||
status_code=self.status_code,
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@ if MCP_AVAILABLE:
|
|||
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
|
||||
global_mcp_server_manager,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.oauth_utils import (
|
||||
get_request_base_url,
|
||||
)
|
||||
from litellm.proxy._experimental.mcp_server.server import (
|
||||
ListMCPToolsRestAPIResponseObject,
|
||||
MCPServer,
|
||||
|
|
@ -437,6 +440,7 @@ if MCP_AVAILABLE:
|
|||
mcp_auth_header: Optional[str],
|
||||
raw_headers_from_request: dict,
|
||||
user_api_key_dict: "UserAPIKeyAuth",
|
||||
request: Request,
|
||||
) -> dict:
|
||||
"""
|
||||
Resolve and fetch tools for a single specified MCP server.
|
||||
|
|
@ -511,7 +515,7 @@ if MCP_AVAILABLE:
|
|||
except MCPUpstreamAuthError as e:
|
||||
# Pass-through server returned 401 — surface it to the client so
|
||||
# standards-compliant MCP clients trigger the upstream OAuth flow.
|
||||
raise e.to_http_exception()
|
||||
raise e.to_http_exception(base_url=get_request_base_url(request))
|
||||
except Exception as e:
|
||||
verbose_logger.exception(f"Error getting tools from {server.name}: {e}")
|
||||
return {
|
||||
|
|
@ -536,6 +540,7 @@ if MCP_AVAILABLE:
|
|||
mcp_auth_header: Optional[str],
|
||||
raw_headers_from_request: dict,
|
||||
user_api_key_dict: UserAPIKeyAuth,
|
||||
request: Request,
|
||||
) -> dict:
|
||||
"""Handle tool listing for a single server_id request."""
|
||||
# Resolve a server name to its UUID if needed
|
||||
|
|
@ -604,7 +609,7 @@ if MCP_AVAILABLE:
|
|||
except MCPUpstreamAuthError as e:
|
||||
# Pass-through server returned 401 — surface it to the client so
|
||||
# standards-compliant MCP clients trigger the upstream OAuth flow.
|
||||
raise e.to_http_exception()
|
||||
raise e.to_http_exception(base_url=get_request_base_url(request))
|
||||
except Exception as e:
|
||||
verbose_logger.exception(f"Error getting tools from {server.name}: {e}")
|
||||
return {
|
||||
|
|
@ -692,6 +697,7 @@ if MCP_AVAILABLE:
|
|||
mcp_auth_header=mcp_auth_header,
|
||||
raw_headers_from_request=raw_headers_from_request,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
request=request,
|
||||
)
|
||||
else:
|
||||
if not allowed_server_ids:
|
||||
|
|
|
|||
|
|
@ -3231,7 +3231,9 @@ if MCP_AVAILABLE:
|
|||
except MCPUpstreamAuthError as e:
|
||||
# Pass-through server returned 401 — surface it to the client so
|
||||
# standards-compliant MCP clients trigger the upstream OAuth flow.
|
||||
raise e.to_http_exception()
|
||||
raise e.to_http_exception(
|
||||
base_url=get_request_base_url(StarletteRequest(scope))
|
||||
)
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions to preserve status codes and details
|
||||
raise
|
||||
|
|
@ -3334,7 +3336,9 @@ if MCP_AVAILABLE:
|
|||
except MCPUpstreamAuthError as e:
|
||||
# Pass-through server returned 401 — surface it to the client so
|
||||
# standards-compliant MCP clients trigger the upstream OAuth flow.
|
||||
raise e.to_http_exception()
|
||||
raise e.to_http_exception(
|
||||
base_url=get_request_base_url(StarletteRequest(scope))
|
||||
)
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions to preserve status codes and details
|
||||
# (e.g. 401 + WWW-Authenticate challenges from OAuth pass-through).
|
||||
|
|
|
|||
|
|
@ -140,6 +140,20 @@ def test_to_http_exception_fabricates_resource_metadata_when_upstream_omits_head
|
|||
}
|
||||
|
||||
|
||||
def test_to_http_exception_fabricates_absolute_resource_metadata_with_base_url():
|
||||
err = MCPUpstreamAuthError(
|
||||
status_code=401,
|
||||
www_authenticate=None,
|
||||
server_name="sample_docs",
|
||||
)
|
||||
|
||||
http_exc = err.to_http_exception(base_url="https://gateway.example.com/")
|
||||
assert http_exc.status_code == 401
|
||||
assert http_exc.headers == {
|
||||
"www-authenticate": 'Bearer resource_metadata="https://gateway.example.com/.well-known/oauth-protected-resource/mcp/sample_docs"'
|
||||
}
|
||||
|
||||
|
||||
def test_to_http_exception_skips_challenge_for_non_401_status():
|
||||
err = MCPUpstreamAuthError(
|
||||
status_code=403,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue