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""" +
+
+ ▮ + Requested Access +
+ {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.

+ + """ @@ -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)