diff --git a/docker/Dockerfile.non_root b/docker/Dockerfile.non_root
index fda591df083..0235cbe7b58 100644
--- a/docker/Dockerfile.non_root
+++ b/docker/Dockerfile.non_root
@@ -53,12 +53,10 @@ RUN mkdir -p /var/lib/litellm/ui && \
mkdir -p /var/lib/litellm/assets && \
cp /app/litellm/proxy/logo.jpg /var/lib/litellm/assets/logo.jpg && \
( cd /var/lib/litellm/ui && \
- for html_file in *.html; do \
- if [ "$html_file" != "index.html" ] && [ -f "$html_file" ]; then \
- folder_name="${html_file%.html}" && \
- mkdir -p "$folder_name" && \
- mv "$html_file" "$folder_name/index.html"; \
- fi; \
+ find . -name "*.html" ! -name "index.html" | while read html_file; do \
+ folder_name="${html_file%.html}" && \
+ mkdir -p "$folder_name" && \
+ mv "$html_file" "$folder_name/index.html"; \
done && \
touch .litellm_ui_ready ) && \
cd /app/ui/litellm-dashboard && rm -rf ./out
diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py
index b731bc7bc2f..e9ec8de83dd 100644
--- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py
+++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py
@@ -420,6 +420,113 @@ async def callback(code: str, state: str):
)
+def _build_mcp_oauth_callback_html(code: Optional[str], state: Optional[str]) -> str:
+ """
+ Build the inline HTML page for the MCP OAuth callback.
+
+ This page mirrors the logic of the Next.js page at
+ ui/litellm-dashboard/src/app/mcp/oauth/callback/page.tsx:
+ it stores the OAuth result in browser storage and redirects the
+ user back to the LiteLLM UI.
+
+ Served as a backend route so it works even when the static-files
+ mount for /ui is unavailable (e.g. read-only container filesystems).
+ """
+ code_json = json.dumps(code)
+ state_json = json.dumps(state)
+ return f"""
+
+
+
+
+ LiteLLM MCP OAuth
+
+
+
+
+
LiteLLM MCP OAuth
+
Authorization complete. You may close this window and return to the LiteLLM dashboard.
+
If the window does not close automatically, everything is still saved — you can close it manually.
+
+
+
+"""
+
+
+@router.get("/ui/mcp/oauth/callback", include_in_schema=False)
+async def mcp_oauth_ui_callback(
+ code: Optional[str] = None,
+ state: Optional[str] = None,
+) -> HTMLResponse:
+ """
+ OAuth callback landing page for the MCP UI flow.
+
+ After the external OAuth provider redirects the browser to /callback,
+ LiteLLM decodes the state and performs a second redirect to this page
+ (the original redirect_uri supplied by the UI).
+
+ This backend route serves an inline HTML page that stores the OAuth
+ result in browser storage and redirects the user back to the LiteLLM
+ dashboard. It acts as a reliable fallback for environments where the
+ static-file mount for /ui is unavailable (read-only container
+ filesystems, Kubernetes deployments, etc.).
+ """
+ return HTMLResponse(content=_build_mcp_oauth_callback_html(code, state))
+
+
# ------------------------------
# Optional .well-known endpoints for MCP + OAuth discovery
# ------------------------------
diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py
index bc2728c2203..f2810be27cc 100644
--- a/litellm/proxy/proxy_server.py
+++ b/litellm/proxy/proxy_server.py
@@ -353,6 +353,9 @@ from litellm.proxy.management_endpoints.common_utils import (
from litellm.proxy.management_endpoints.compliance_endpoints import (
router as compliance_router,
)
+from litellm.proxy.management_endpoints.config_override_endpoints import (
+ router as config_override_router,
+)
from litellm.proxy.management_endpoints.cost_tracking_settings import (
router as cost_tracking_settings_router,
)
@@ -368,6 +371,9 @@ from litellm.proxy.management_endpoints.internal_user_endpoints import (
from litellm.proxy.management_endpoints.internal_user_endpoints import (
user_update,
)
+from litellm.proxy.management_endpoints.jwt_key_mapping_endpoints import (
+ router as jwt_key_mapping_router,
+)
from litellm.proxy.management_endpoints.key_management_endpoints import (
delete_verification_tokens,
duration_in_seconds,
@@ -584,6 +590,7 @@ from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from fastapi.responses import (
FileResponse,
+ HTMLResponse,
JSONResponse,
ORJSONResponse,
RedirectResponse,
@@ -1331,6 +1338,17 @@ try:
)
# print(f"mounted _next at {server_root_path}/ui/_next")
+ # Register the MCP OAuth callback route BEFORE mounting the /ui StaticFiles
+ from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
+ _build_mcp_oauth_callback_html,
+ )
+
+ @app.get("/ui/mcp/oauth/callback", include_in_schema=False)
+ async def _mcp_oauth_ui_callback(
+ code: Optional[str] = None, state: Optional[str] = None
+ ) -> HTMLResponse:
+ return HTMLResponse(content=_build_mcp_oauth_callback_html(code, state))
+
app.mount("/ui", StaticFiles(directory=ui_path, html=True), name="ui")
def _restructure_ui_html_files(ui_root: str) -> None:
diff --git a/tests/litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py
index ac2c945baa6..78c703ad4d9 100644
--- a/tests/litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py
+++ b/tests/litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py
@@ -1,22 +1,24 @@
"""Tests for MCP OAuth discoverable endpoints"""
-import pytest
from unittest.mock import AsyncMock, MagicMock, patch
+import pytest
+
@pytest.mark.asyncio
async def test_authorize_endpoint_includes_response_type():
"""Test that authorize endpoint includes response_type=code parameter (fixes #15684)"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
authorize,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -73,16 +75,17 @@ async def test_authorize_endpoint_includes_response_type():
async def test_authorize_endpoint_forwards_pkce_parameters():
"""Test that authorize endpoint forwards PKCE parameters (code_challenge and code_challenge_method)"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
authorize,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -143,17 +146,18 @@ async def test_authorize_endpoint_forwards_pkce_parameters():
async def test_token_endpoint_forwards_code_verifier():
"""Test that token endpoint forwards code_verifier for PKCE flow"""
try:
+ import httpx
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
token_endpoint,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
- import httpx
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -240,10 +244,11 @@ async def test_token_endpoint_forwards_code_verifier():
@pytest.mark.asyncio
async def test_register_client_without_mcp_server_name_returns_dummy():
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
register_client,
)
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -266,16 +271,17 @@ async def test_register_client_without_mcp_server_name_returns_dummy():
@pytest.mark.asyncio
async def test_register_client_returns_existing_server_credentials():
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
register_client,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -319,16 +325,17 @@ async def test_register_client_returns_existing_server_credentials():
@pytest.mark.asyncio
async def test_register_client_remote_registration_success():
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
register_client,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -409,16 +416,17 @@ async def test_register_client_remote_registration_success():
async def test_authorize_endpoint_respects_x_forwarded_proto():
"""Test that authorize endpoint uses X-Forwarded-Proto header to construct correct redirect_uri"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
authorize,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -475,16 +483,17 @@ async def test_authorize_endpoint_respects_x_forwarded_proto():
async def test_token_endpoint_respects_x_forwarded_proto():
"""Test that token endpoint uses X-Forwarded-Proto header for redirect_uri"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
token_endpoint,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -553,16 +562,17 @@ async def test_token_endpoint_respects_x_forwarded_proto():
async def test_oauth_protected_resource_standard_pattern():
"""Test that oauth_protected_resource_mcp_standard returns standard MCP URL pattern (/mcp/{server_name})"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
oauth_protected_resource_mcp_standard,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -606,16 +616,17 @@ async def test_oauth_protected_resource_standard_pattern():
async def test_oauth_protected_resource_legacy_pattern():
"""Test that oauth_protected_resource_mcp returns legacy URL pattern (/{server_name}/mcp)"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
oauth_protected_resource_mcp,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -659,16 +670,17 @@ async def test_oauth_protected_resource_legacy_pattern():
async def test_oauth_protected_resource_respects_x_forwarded_proto():
"""Test that oauth_protected_resource_mcp uses X-Forwarded-Proto for URLs"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
oauth_protected_resource_mcp,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
# Clear registry
@@ -712,16 +724,17 @@ async def test_oauth_protected_resource_respects_x_forwarded_proto():
async def test_oauth_authorization_server_respects_x_forwarded_proto():
"""Test that oauth_authorization_server_mcp uses X-Forwarded-Proto for URLs"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
oauth_authorization_server_mcp,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
# Clear registry
@@ -766,10 +779,11 @@ async def test_oauth_authorization_server_respects_x_forwarded_proto():
async def test_register_client_respects_x_forwarded_proto():
"""Test that register_client uses X-Forwarded-Proto for redirect_uris"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
register_client,
)
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -796,16 +810,17 @@ async def test_register_client_respects_x_forwarded_proto():
async def test_authorize_endpoint_respects_x_forwarded_host():
"""Test that authorize endpoint uses X-Forwarded-Host and X-Forwarded-Proto to construct correct redirect_uri"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
authorize,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -868,16 +883,17 @@ async def test_authorize_endpoint_respects_x_forwarded_host():
async def test_token_endpoint_respects_x_forwarded_host():
"""Test that token endpoint uses X-Forwarded-Host and X-Forwarded-Proto for redirect_uri"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
token_endpoint,
)
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
global_mcp_server_manager,
)
+ 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._types import MCPTransport
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -1069,10 +1085,11 @@ def test_get_request_base_url_comprehensive(
):
"""Comprehensive test for get_request_base_url with various header combinations"""
try:
+ from fastapi import Request
+
from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
get_request_base_url,
)
- from fastapi import Request
except ImportError:
pytest.skip("MCP discoverable endpoints not available")
@@ -1106,3 +1123,62 @@ def test_get_request_base_url_comprehensive(
f"X-Forwarded-Host={x_forwarded_host}, "
f"X-Forwarded-Port={x_forwarded_port}"
)
+
+
+
+# ---------------------------------------------------------------------------
+# Tests for /ui/mcp/oauth/callback backend endpoint
+# ---------------------------------------------------------------------------
+
+
+@pytest.mark.asyncio
+async def test_mcp_oauth_ui_callback_returns_html():
+ """
+ The /ui/mcp/oauth/callback endpoint must return an HTML page regardless
+ of whether the static files mount is available. This is the fallback
+ that prevents a 404 in Kubernetes/read-only deployments.
+ """
+ try:
+ from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
+ mcp_oauth_ui_callback,
+ )
+ except ImportError:
+ pytest.skip("MCP discoverable endpoints not available")
+
+ response = await mcp_oauth_ui_callback(
+ code="test_auth_code_123",
+ state="test_state_456",
+ )
+
+ assert response.status_code == 200
+ body = response.body.decode()
+ assert "" in body
+ # Payload values are embedded as JSON literals in the JS
+ assert "test_auth_code_123" in body
+ assert "test_state_456" in body
+ # Verify the storage key constants match the Next.js page
+ assert "litellm-mcp-oauth-result" in body
+ assert "litellm-mcp-oauth-return-url" in body
+
+
+@pytest.mark.asyncio
+async def test_mcp_oauth_ui_callback_handles_missing_params():
+ """
+ The /ui/mcp/oauth/callback endpoint must handle missing code/state
+ gracefully (e.g. direct navigation or error redirect from provider).
+ """
+ try:
+ from litellm.proxy._experimental.mcp_server.discoverable_endpoints import (
+ mcp_oauth_ui_callback,
+ )
+ except ImportError:
+ pytest.skip("MCP discoverable endpoints not available")
+
+ response = await mcp_oauth_ui_callback(code=None, state=None)
+
+ assert response.status_code == 200
+ body = response.body.decode()
+ assert "" in body
+ # null values should be safely embedded as JSON null
+ assert "null" in body
+