Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
d8beb7d
fix(proxy): point /metrics 401 at the opt-out flag
yuneng-berri May 9, 2026
7e0347c
Merge pull request #27505 from BerriAI/cherry-pick-metrics-optout-hin…
yuneng-berri May 9, 2026
a0b2811
fix(proxy): bound budget reservation per request instead of pinning t…
yuneng-berri May 9, 2026
b347015
fix(proxy): reserve per-image cost for image-generation requests
yuneng-berri May 9, 2026
d50c98d
fix(proxy): gate image-gen reservation strictly on model mode
yuneng-berri May 9, 2026
18c14d9
Merge pull request #27539 from BerriAI/litellm_/budget-reservation-rc…
yuneng-berri May 9, 2026
82c8cb8
build(packaging): relax core runtime pins to ranges
yuneng-berri May 9, 2026
5bb2803
Merge pull request #27545 from BerriAI/litellm_/charming-sammet-8d77c3
yuneng-berri May 9, 2026
a5cc4ff
build(packaging): raise jinja2 floor to 3.1.6
yuneng-berri May 9, 2026
188875b
Merge pull request #27554 from BerriAI/litellm_/charming-sammet-8d77c3
yuneng-berri May 10, 2026
38bc921
fix(mcp): forward extra_headers for OpenAPI MCP tools
milan-berri May 7, 2026
c31ead8
refactor(mcp): access has_client_credentials on MCPServer directly
milan-berri May 7, 2026
a1d1906
fix(mcp): static headers win over forwarded headers in OpenAPI MCP
cursoragent May 9, 2026
4046cb1
Merge pull request #27768 from milan-berri/litellm_cherry-pick-27383-…
yuneng-berri May 12, 2026
111ee52
fix(proxy): always merge caller-supplied tags into request metadata
yuneng-berri May 12, 2026
cd9d312
docs(proxy): refresh stale comments referencing removed tag strip
yuneng-berri May 12, 2026
74bb525
Merge pull request #27789 from BerriAI/litellm_/wizardly-sammet-94baac
yuneng-berri May 12, 2026
590bc83
chore: reject bare str at file-input sinks to prevent local-file read…
krrish-berri-2 May 12, 2026
037d4c5
Merge pull request #27794 from BerriAI/litellm_/fervent-archimedes-7e…
yuneng-berri May 13, 2026
2675b50
Strip SERVER_ROOT_PATH before lazy-feature prefix match
yuneng-berri May 13, 2026
c14cab9
Cache normalized SERVER_ROOT_PATH at middleware init
yuneng-berri May 13, 2026
069469c
chore(proxy): backport /key/regenerate ownership-rebind + premium-gat…
yuneng-berri May 13, 2026
8b90a54
Merge pull request #27818 from BerriAI/litellm_backport_27812_to_1840rc2
yuneng-berri May 13, 2026
2b189be
Merge pull request #27819 from BerriAI/litellm_backport_regenerate_gu…
yuneng-berri May 13, 2026
be37cf7
fix(proxy): expose db status on public /health/readiness
yuneng-berri May 13, 2026
be0526b
Merge pull request #27868 from BerriAI/backport-pr-27866-into-1.84.0rc2
yuneng-berri May 13, 2026
ab0267d
fix(ui): fetch version + debug flag from /health/readiness/details
yuneng-berri May 14, 2026
51ed234
fix(ui): disable retries on readiness/details + cover token forwarding
yuneng-berri May 14, 2026
305875f
Merge pull request #27899 from BerriAI/backport-pr-27896-into-1.84.0rc2
yuneng-berri May 14, 2026
02c5e1c
chore: update Next.js build artifacts (2026-05-14 03:52 UTC, node v20…
yuneng-berri May 14, 2026
50030e3
Merge pull request #27901 from BerriAI/yj_build_may13
yuneng-berri May 14, 2026
974f5cd
Merge pull request #27898 from stuxf/chore/banned-params-extra-body-c…
yuneng-berri May 14, 2026
3dd763d
Merge pull request #27801 from stuxf/chore/get-instance-fn-runtime-s3…
yuneng-berri May 14, 2026
08ea016
Merge pull request #27902 from BerriAI/litellm_/eager-euler-fd3639
yuneng-berri May 14, 2026
fdb55ab
fix: block client-side pricing injection via request body
krrish-berri-2 May 14, 2026
b2c93b1
fix: block SSRF fields in RAG ingest vector_store config
krrish-berri-2 May 14, 2026
d35d2a7
fix: harden /key/update authorization checks (#27878)
krrish-berri-2 May 14, 2026
087003e
Merge pull request #27903 from BerriAI/backport/27892-litellm_1.84.0rc2
yuneng-berri May 14, 2026
fe18665
Merge remote-tracking branch 'origin/litellm_1.84.0rc2' into backport…
yuneng-berri May 14, 2026
321d576
Merge pull request #27904 from BerriAI/backport/27878-litellm_1.84.0rc2
yuneng-berri May 14, 2026
0f741fc
bump: version 0.4.71 → 0.4.72
yuneng-berri May 14, 2026
fcd63b0
uv lock
yuneng-berri May 14, 2026
e1fc955
Merge pull request #27909 from BerriAI/backport/27908-litellm_1.84.0rc2
yuneng-berri May 14, 2026
d0cc1a4
feat(mcp): support OAuth passthrough discovery
gym-cmd May 15, 2026
0daeeab
fix(mcp): support OAuth browser auth
gym-cmd May 15, 2026
8e864f3
fix(mcp): refine upstream OAuth metadata fallback
gym-cmd May 15, 2026
da24c89
feat(proxy): support issuer-scoped JWT auth
gym-cmd May 15, 2026
c279f4e
fix(mcp): validate oauth callback redirect sink
gym-cmd May 15, 2026
1d731af
feat(proxy): support issuer-scoped JWT auth
gym-cmd May 15, 2026
dee5d55
test(mcp): align trusted proxy fixtures
gym-cmd May 15, 2026
dfb1e33
style(mcp): satisfy black formatting
gym-cmd May 15, 2026
b290416
chore(ui): bump next to 16.2.6
gym-cmd May 15, 2026
8ca2f14
fix(mcp): address oauth passthrough review findings
gym-cmd May 15, 2026
6079a5f
test(mcp): split oauth passthrough regressions
gym-cmd May 15, 2026
409a005
fix(interactions): align openapi response fields
gym-cmd May 15, 2026
3753970
security: prevent forwarding litellm api keys to upstream mcp servers
gym-cmd May 16, 2026
3990165
fix(interactions): remove steps field not in google openapi spec
gym-cmd May 16, 2026
f6359cb
fix(mcp): forward Authorization in pass-through when x-litellm-api-ke…
artem-dudarev-jet May 19, 2026
216e055
fix(interactions): align status enum with openapi spec
gym-cmd May 20, 2026
b4df1a9
Merge remote-tracking branch 'upstream/litellm_internal_staging' into…
gym-cmd May 20, 2026
eae390b
fix(mcp): respect client ip for delegated auth
gym-cmd May 20, 2026
96e4de3
style(mcp): format discoverable endpoints
gym-cmd May 20, 2026
2636bbc
test(proxy): cover issuer-scoped JWT auth
gym-cmd May 20, 2026
0447ba6
fix(mcp): use resource metadata for passthrough reauth
gym-cmd May 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions litellm/experimental_mcp_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,16 @@ def factory(

return factory

async def list_tools(self) -> List[MCPTool]:
"""List available tools from the server."""
async def list_tools(self, raise_on_error: bool = False) -> List[MCPTool]:
"""List available tools from the server.

Args:
raise_on_error: When True, re-raise exceptions instead of returning
an empty list. Used by the proxy's pass-through MCP flow so it
can surface upstream HTTP 401 responses as a proper 401 to the
MCP client (triggering the upstream OAuth flow) rather than
masking them as "connected, no tools".
"""
verbose_logger.debug(
f"MCP client listing tools from {self.server_url or 'stdio'}"
)
Expand Down Expand Up @@ -458,6 +466,8 @@ async def _list_tools_operation(session: ClientSession):
"the MCP server may have crashed, disconnected, or timed out"
)

if raise_on_error:
raise
# Return empty list instead of raising to allow graceful degradation
return []

Expand Down
109 changes: 99 additions & 10 deletions litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,59 @@
UserAPIKeyAuth,
)
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.auth.ip_address_utils import IPAddressUtils


def _parse_mcp_server_names_from_path(path: str) -> Optional[List[str]]:
"""Parse a single MCP server name from /mcp/{name} or /{name}/mcp path patterns.
Returns None for the aggregate /mcp route (no bypass for multi-server paths)."""
m = re.match(r"^/mcp/([^/,?#]+)", path)
if m:
return [m.group(1)]
m = re.match(r"^/([^/,?#]+)/mcp", path)
if m:
return [m.group(1)]
return None


def _is_mcp_passthrough_cold_start(
scope: Scope, mcp_servers: Optional[List[str]], client_ip: Optional[str]
) -> bool:
"""True when the request targets a pass-through server with no auth headers —
the cold-start OAuth discovery case per RFC 9728 / MCP Authorization spec.
Lets the route handler's 401 emitter produce the spec-compliant WWW-Authenticate
challenge instead of surfacing a generic admission error."""
if not mcp_servers:
return False
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)

for name in mcp_servers:
server = global_mcp_server_manager.get_mcp_server_by_name(
name, client_ip=client_ip
)
if server is not None and getattr(server, "is_oauth_passthrough", False):
return True
return False


def _is_litellm_auth_admission_error(exc: Exception) -> bool:
if isinstance(exc, HTTPException):
return exc.status_code == 401
if isinstance(exc, ProxyException):
try:
return int(exc.code) == 401
except (TypeError, ValueError):
return False
return False


def _has_client_supplied_mcp_auth(
mcp_auth_header: Optional[str],
mcp_server_auth_headers: Dict[str, Dict[str, str]],
) -> bool:
return bool(mcp_auth_header) or bool(mcp_server_auth_headers)


class MCPRequestHandler:
Expand All @@ -37,7 +90,7 @@ class MCPRequestHandler:
LITELLM_MCP_ACCESS_GROUPS_HEADER_NAME = SpecialHeaders.mcp_access_groups.value

@staticmethod
async def process_mcp_request(
async def process_mcp_request( # noqa: PLR0915
scope: Scope,
) -> Tuple[
UserAPIKeyAuth,
Expand Down Expand Up @@ -126,7 +179,9 @@ async def mock_body():
elif (
not litellm_api_key
and MCPRequestHandler._target_servers_delegate_auth_to_upstream( # noqa: E501
path=request.url.path, mcp_servers=mcp_servers
path=request.url.path,
mcp_servers=mcp_servers,
client_ip=IPAddressUtils.get_mcp_client_ip(request),
)
):
# Operator opted this oauth2 server into upstream-delegated auth
Expand Down Expand Up @@ -174,7 +229,9 @@ async def mock_body():
"401",
"403",
) and MCPRequestHandler._target_servers_use_oauth2(
path=request.url.path, mcp_servers=mcp_servers
path=request.url.path,
mcp_servers=mcp_servers,
client_ip=IPAddressUtils.get_mcp_client_ip(request),
):
verbose_logger.debug(
"MCP OAuth2: target server is OAuth2-mode, treating "
Expand All @@ -184,9 +241,35 @@ async def mock_body():
else:
raise
else:
validated_user_api_key_auth = await user_api_key_auth(
api_key=litellm_api_key, request=request
)
try:
validated_user_api_key_auth = await user_api_key_auth(
api_key=litellm_api_key, request=request
)
except (HTTPException, ProxyException) as exc:
# Cold-start MCP OAuth discovery: RFC 9728 / MCP Authorization spec
# require unauthenticated requests to protected resources to receive
# 401 + WWW-Authenticate. Defer to _raise_preemptive_401_for_unauthenticated_servers
# for pass-through servers instead of surfacing a generic admission error.
path = scope.get("path", "")
mcp_servers_from_path = _parse_mcp_server_names_from_path(path)
client_ip = IPAddressUtils.get_mcp_client_ip(request)
if (
mcp_servers_from_path is not None
and not _has_client_supplied_mcp_auth(
mcp_auth_header,
mcp_server_auth_headers,
)
and _is_litellm_auth_admission_error(exc)
and _is_mcp_passthrough_cold_start(
scope, mcp_servers_from_path, client_ip=client_ip
)
):
verbose_logger.debug(
"MCP pass-through cold start: deferring admission to route 401 emitter"
)
validated_user_api_key_auth = UserAPIKeyAuth()
else:
raise
Comment thread
gym-cmd marked this conversation as resolved.

return (
validated_user_api_key_auth,
Expand Down Expand Up @@ -258,7 +341,9 @@ def _extract_target_server_names_from_path(path: str) -> List[str]:
return [servers_and_path]

@staticmethod
def _target_servers_use_oauth2(path: str, mcp_servers: Optional[List[str]]) -> bool:
def _target_servers_use_oauth2(
path: str, mcp_servers: Optional[List[str]], client_ip: Optional[str]
) -> bool:
"""
True only when EVERY MCP server the request targets is configured for
``auth_type == oauth2``. If any target is non-OAuth2 — or if the target
Expand Down Expand Up @@ -287,14 +372,16 @@ def _target_servers_use_oauth2(path: str, mcp_servers: Optional[List[str]]) -> b
return False

for name in target_names:
server = global_mcp_server_manager.get_mcp_server_by_name(name)
server = global_mcp_server_manager.get_mcp_server_by_name(
name, client_ip=client_ip
)
if server is None or server.auth_type != MCPAuth.oauth2:
return False
return True

@staticmethod
def _target_servers_delegate_auth_to_upstream(
path: str, mcp_servers: Optional[List[str]]
path: str, mcp_servers: Optional[List[str]], client_ip: Optional[str]
) -> bool:
"""
True only when EVERY MCP server the request targets is configured for
Expand Down Expand Up @@ -324,7 +411,9 @@ def _target_servers_delegate_auth_to_upstream(
return False

for name in target_names:
server = global_mcp_server_manager.get_mcp_server_by_name(name)
server = global_mcp_server_manager.get_mcp_server_by_name(
name, client_ip=client_ip
)
if server is None or server.auth_type != MCPAuth.oauth2:
return False
# `is True` is intentional: opt-in must be an explicit boolean
Expand Down
Loading
Loading