mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(mcp): MCP OAuth callback 404, scope pollution, and Docker pre-build
- Add backend route /ui/mcp/oauth/callback to serve OAuth callback HTML when static files are unavailable (older images, pip install, read-only fs) - Fix OAuth scope bug: do not auto-append discovered scopes when user leaves scope field empty (was causing GitLab 'invalid scope' errors) - Fix Dockerfile.non_root: restructure nested HTML files recursively (mcp/oauth/callback.html was missed by top-level-only glob) - Add tests for scope handling and callback endpoint Made-with: Cursor
This commit is contained in:
parent
a8cf646850
commit
6f49645eb5
4 changed files with 236 additions and 37 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>LiteLLM MCP OAuth</title>
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}}
|
||||
.card {{
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||||
padding: 2.5rem 2rem;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}}
|
||||
h1 {{ font-size: 1.25rem; color: #0f172a; margin-bottom: 0.75rem; }}
|
||||
p {{ font-size: 0.9rem; color: #475569; line-height: 1.6; margin: 0.25rem 0; }}
|
||||
.note {{ font-size: 0.75rem; color: #94a3b8; margin-top: 0.5rem; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>LiteLLM MCP OAuth</h1>
|
||||
<p>Authorization complete. You may close this window and return to the LiteLLM dashboard.</p>
|
||||
<p class="note">If the window does not close automatically, everything is still saved — you can close it manually.</p>
|
||||
</div>
|
||||
<script>
|
||||
(function () {{
|
||||
var RESULT_KEY = "litellm-mcp-oauth-result";
|
||||
var RETURN_URL_KEY = "litellm-mcp-oauth-return-url";
|
||||
|
||||
var payload = {{
|
||||
type: "litellm-mcp-oauth",
|
||||
code: {code_json},
|
||||
state: {state_json}
|
||||
}};
|
||||
|
||||
try {{
|
||||
var payloadStr = JSON.stringify(payload);
|
||||
window.sessionStorage.setItem(RESULT_KEY, payloadStr);
|
||||
window.localStorage.setItem(RESULT_KEY, payloadStr);
|
||||
}} catch (e) {{}}
|
||||
|
||||
var returnUrl = null;
|
||||
try {{
|
||||
returnUrl = window.sessionStorage.getItem(RETURN_URL_KEY) ||
|
||||
window.localStorage.getItem(RETURN_URL_KEY);
|
||||
}} catch (e) {{}}
|
||||
|
||||
if (!returnUrl) {{
|
||||
var path = window.location.pathname || "";
|
||||
var uiIndex = path.indexOf("/ui");
|
||||
returnUrl = uiIndex >= 0 ? path.slice(0, uiIndex + 3) : "/";
|
||||
}}
|
||||
|
||||
window.location.replace(returnUrl);
|
||||
}})();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
@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
|
||||
# ------------------------------
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 "<!DOCTYPE html>" 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 "<!DOCTYPE html>" in body
|
||||
# null values should be safely embedded as JSON null
|
||||
assert "null" in body
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue