diff --git a/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py
index ea4c8133064..d4d327f0691 100644
--- a/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py
+++ b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py
@@ -65,41 +65,439 @@ def _purge_expired_codes() -> None:
del _byok_auth_codes[k]
-# ---------------------------------------------------------------------------
-# HTML template for the authorization page
-# ---------------------------------------------------------------------------
+def _build_authorize_html(
+ server_name: str,
+ server_initial: str,
+ client_id: str,
+ redirect_uri: str,
+ code_challenge: str,
+ code_challenge_method: str,
+ state: str,
+ server_id: str,
+ access_items: list,
+ help_url: str,
+) -> str:
+ """Build the 2-step BYOK OAuth authorization page HTML."""
-_AUTHORIZE_HTML = """
-
-
Connect to {server_name} — LiteLLM
+ # Build access checklist rows
+ access_rows = "".join(
+ f'✓{item}
'
+ for item in access_items
+ )
+ access_section = ""
+ if access_rows:
+ access_section = f"""
+
+
+ {access_rows}
+
"""
+
+ # Help link for step 2
+ help_link_html = ""
+ if help_url:
+ help_link_html = f'Where do I find my API key? ↗'
+
+ return f"""
+
+
+
+
+Connect {server_name} — LiteLLM
+ *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
+ body {{
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #0f172a;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+ }}
+ .modal {{
+ background: #ffffff;
+ border-radius: 20px;
+ padding: 36px 32px 32px;
+ width: 440px;
+ max-width: 100%;
+ position: relative;
+ box-shadow: 0 25px 60px rgba(0,0,0,0.35);
+ }}
+ /* Progress dots */
+ .dots {{
+ display: flex;
+ justify-content: center;
+ gap: 7px;
+ margin-bottom: 28px;
+ }}
+ .dot {{
+ width: 8px; height: 8px;
+ border-radius: 50%;
+ background: #e2e8f0;
+ }}
+ .dot.active {{ background: #38bdf8; }}
+ /* Close button */
+ .close-btn {{
+ position: absolute;
+ top: 16px; right: 16px;
+ background: none; border: none;
+ font-size: 16px; color: #94a3b8;
+ cursor: pointer; line-height: 1;
+ width: 28px; height: 28px;
+ border-radius: 6px;
+ display: flex; align-items: center; justify-content: center;
+ }}
+ .close-btn:hover {{ background: #f1f5f9; color: #475569; }}
+ /* Logo pair */
+ .logos {{
+ display: flex; align-items: center; justify-content: center;
+ gap: 12px; margin-bottom: 20px;
+ }}
+ .logo {{
+ width: 52px; height: 52px;
+ border-radius: 14px;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 22px; font-weight: 800; color: white;
+ }}
+ .logo-l {{ background: linear-gradient(135deg, #38bdf8 0%, #0284c7 100%); }}
+ .logo-s {{ background: linear-gradient(135deg, #818cf8 0%, #4f46e5 100%); }}
+ .logo-arrow {{ color: #cbd5e1; font-size: 20px; font-weight: 300; }}
+ /* Headings */
+ .step-title {{
+ text-align: center;
+ font-size: 21px; font-weight: 700;
+ color: #0f172a; margin-bottom: 8px;
+ }}
+ .step-subtitle {{
+ text-align: center;
+ font-size: 14px; color: #64748b;
+ line-height: 1.55; margin-bottom: 22px;
+ }}
+ /* Info box */
+ .info-box {{
+ background: #f8fafc;
+ border-radius: 12px;
+ padding: 14px 16px;
+ display: flex; gap: 12px;
+ margin-bottom: 14px;
+ }}
+ .info-icon {{ font-size: 17px; flex-shrink: 0; margin-top: 1px; color: #38bdf8; }}
+ .info-box h4 {{ font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 4px; }}
+ .info-box p {{ font-size: 13px; color: #64748b; line-height: 1.5; }}
+ /* Access checklist */
+ .access-box {{
+ background: #f8fafc;
+ border-radius: 12px;
+ padding: 14px 16px;
+ margin-bottom: 22px;
+ }}
+ .access-header {{
+ display: flex; align-items: center; gap: 8px;
+ margin-bottom: 10px;
+ }}
+ .shield {{ color: #22c55e; font-size: 15px; }}
+ .access-header > span:last-child {{
+ font-size: 11px; font-weight: 700;
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+ color: #475569;
+ }}
+ .access-item {{
+ display: flex; align-items: center; gap: 9px;
+ font-size: 13.5px; color: #374151;
+ padding: 3px 0;
+ }}
+ .check {{ color: #22c55e; font-weight: 700; font-size: 13px; }}
+ /* Primary CTA */
+ .btn-primary {{
+ width: 100%; padding: 15px;
+ background: #0f172a; color: white;
+ border: none; border-radius: 12px;
+ font-size: 15px; font-weight: 600;
+ cursor: pointer; margin-bottom: 10px;
+ }}
+ .btn-primary:hover {{ background: #1e293b; }}
+ .btn-cancel {{
+ width: 100%; padding: 8px;
+ background: none; border: none;
+ font-size: 13.5px; color: #94a3b8;
+ cursor: pointer;
+ }}
+ .btn-cancel:hover {{ color: #64748b; }}
+ /* Step 2 nav */
+ .step2-nav {{
+ display: flex; align-items: center;
+ justify-content: space-between;
+ margin-bottom: 24px;
+ }}
+ .back-btn {{
+ background: none; border: none;
+ font-size: 13.5px; color: #64748b;
+ cursor: pointer; display: flex; align-items: center; gap: 4px;
+ }}
+ .back-btn:hover {{ color: #374151; }}
+ /* Key icon */
+ .key-icon-wrap {{
+ width: 46px; height: 46px;
+ background: #e0f2fe;
+ border-radius: 12px;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 20px; margin-bottom: 14px;
+ }}
+ /* Form elements */
+ .field-label {{
+ font-size: 13.5px; font-weight: 600;
+ color: #1e293b; display: block;
+ margin-bottom: 7px;
+ }}
+ .key-input {{
+ width: 100%; padding: 11px 13px;
+ border: 1.5px solid #e2e8f0;
+ border-radius: 10px;
+ font-size: 14px; color: #0f172a;
+ outline: none; transition: border-color 0.15s, box-shadow 0.15s;
+ }}
+ .key-input:focus {{
+ border-color: #38bdf8;
+ box-shadow: 0 0 0 3px rgba(56,189,248,0.12);
+ }}
+ .help-link {{
+ display: inline-flex; align-items: center; gap: 4px;
+ color: #0ea5e9; font-size: 13px;
+ text-decoration: none; margin: 8px 0 16px;
+ }}
+ .help-link:hover {{ text-decoration: underline; }}
+ /* Save toggle card */
+ .save-card {{
+ border: 1.5px solid #e2e8f0;
+ border-radius: 12px;
+ padding: 13px 15px;
+ margin-bottom: 6px;
+ }}
+ .save-row {{
+ display: flex; align-items: center; gap: 10px;
+ }}
+ .save-icon {{ font-size: 16px; }}
+ .save-label {{
+ flex: 1;
+ font-size: 14px; font-weight: 500; color: #1e293b;
+ }}
+ /* Toggle switch */
+ .toggle {{ position: relative; width: 44px; height: 24px; flex-shrink: 0; }}
+ .toggle input {{ opacity: 0; width: 0; height: 0; }}
+ .slider {{
+ position: absolute; inset: 0;
+ background: #e2e8f0;
+ border-radius: 24px; cursor: pointer;
+ transition: background 0.18s;
+ }}
+ .slider::before {{
+ content: '';
+ position: absolute;
+ width: 18px; height: 18px;
+ left: 3px; bottom: 3px;
+ background: white;
+ border-radius: 50%;
+ transition: transform 0.18s;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.18);
+ }}
+ input:checked + .slider {{ background: #38bdf8; }}
+ input:checked + .slider::before {{ transform: translateX(20px); }}
+ /* Duration pills */
+ .duration-section {{ margin-top: 14px; }}
+ .duration-label {{
+ font-size: 12px; font-weight: 600;
+ color: #64748b; margin-bottom: 8px;
+ text-transform: uppercase; letter-spacing: 0.05em;
+ }}
+ .pills {{ display: flex; flex-wrap: wrap; gap: 7px; }}
+ .pill {{
+ padding: 6px 13px;
+ border: 1.5px solid #e2e8f0;
+ border-radius: 20px;
+ font-size: 13px; color: #475569;
+ cursor: pointer; background: white;
+ transition: all 0.13s;
+ user-select: none;
+ }}
+ .pill:hover {{ border-color: #94a3b8; }}
+ .pill.sel {{
+ border-color: #38bdf8;
+ color: #0284c7;
+ background: #e0f2fe;
+ }}
+ /* Security note */
+ .sec-note {{
+ background: #f8fafc;
+ border-radius: 10px;
+ padding: 11px 14px;
+ display: flex; gap: 9px; align-items: flex-start;
+ margin: 16px 0;
+ }}
+ .sec-icon {{ font-size: 13px; color: #94a3b8; margin-top: 1px; flex-shrink: 0; }}
+ .sec-note p {{ font-size: 12.5px; color: #64748b; line-height: 1.5; }}
+ /* Connect button */
+ .btn-connect {{
+ width: 100%; padding: 15px;
+ border: none; border-radius: 12px;
+ font-size: 15px; font-weight: 600;
+ cursor: pointer;
+ background: #bae6fd; color: #0369a1;
+ transition: background 0.15s, color 0.15s;
+ }}
+ .btn-connect.ready {{
+ background: #0ea5e9; color: white;
+ }}
+ .btn-connect.ready:hover {{ background: #0284c7; }}
+ /* Step visibility */
+ .step {{ display: none; }}
+ .step.show {{ display: block; }}
+
+
-
-
Connect to {server_name}
-
Enter your {server_name} API key to authorize this connection.
-
-
Your key is encrypted at rest and never shared with third parties.
+
+
+
+
+
+
+
+
+
L
+
→
+
{server_initial}
+
+
+
Connect {server_name}
+
LiteLLM needs access to {server_name} to complete your request.
+
+
+
ⓘ
+
+
How it works
+
LiteLLM acts as a secure bridge. Your requests are routed through our MCP client directly to {server_name}’s API.
+
+
+
+ {access_section}
+
+
+
+
+
+
+
+
+
🔑
+
Provide API Key
+
Enter your {server_name} API key to authorize this connection.
+
+
+
+
+
+
"""
@@ -166,8 +564,10 @@ async def byok_authorize_get(
if not code_challenge:
raise HTTPException(status_code=400, detail="code_challenge is required")
- # Resolve a human-readable server name.
- server_name = server_id or "MCP Server"
+ # Resolve server metadata (name, description items, help URL).
+ server_name = "MCP Server"
+ access_items: list = []
+ help_url = ""
if server_id:
try:
from litellm.proxy._experimental.mcp_server.mcp_server_manager import (
@@ -176,18 +576,26 @@ async def byok_authorize_get(
registry = global_mcp_server_manager.get_registry()
if server_id in registry:
- server_name = registry[server_id].server_name or registry[server_id].name
+ srv = registry[server_id]
+ server_name = srv.server_name or srv.name
+ access_items = list(srv.byok_description or [])
+ help_url = srv.byok_api_key_help_url or ""
except Exception:
pass
- html = _AUTHORIZE_HTML.format(
+ server_initial = (server_name[0].upper()) if server_name else "S"
+
+ html = _build_authorize_html(
server_name=server_name,
+ server_initial=server_initial,
client_id=client_id or "",
redirect_uri=redirect_uri,
code_challenge=code_challenge,
code_challenge_method=code_challenge_method or "S256",
state=state or "",
server_id=server_id or "",
+ access_items=access_items,
+ help_url=help_url,
)
return HTMLResponse(content=html)