"""OAuth Proxy UI Generation Functions. This module contains HTML generation functions for consent and error pages. """ from __future__ import annotations from fastmcp.utilities.ui import ( BUTTON_STYLES, DETAIL_BOX_STYLES, DETAILS_STYLES, INFO_BOX_STYLES, REDIRECT_SECTION_STYLES, TOOLTIP_STYLES, create_logo, create_page, ) def create_consent_html( client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = "Application Access Request", server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None, csp_policy: str | None = None, is_cimd_client: bool = False, cimd_domain: str | None = None, ) -> str: """Create a styled HTML consent page for OAuth authorization requests. Args: csp_policy: Content Security Policy override. If None, uses the built-in CSP policy with appropriate directives. If empty string "", disables CSP entirely (no meta tag is rendered). If a non-empty string, uses that as the CSP policy value. """ import html as html_module client_display = html_module.escape(client_name or client_id) server_name_escaped = html_module.escape(server_name or "FastMCP") # Make server name a hyperlink if website URL is available if server_website_url: website_url_escaped = html_module.escape(server_website_url) server_display = f'{server_name_escaped}' else: server_display = server_name_escaped # Build intro box with call-to-action intro_box = f"""

The application {client_display} wants to access the MCP server {server_display}. Please ensure you recognize the callback address below.

""" # Build CIMD verified domain badge if applicable cimd_badge = "" if is_cimd_client and cimd_domain: cimd_domain_escaped = html_module.escape(cimd_domain) cimd_badge = f"""
✓ Verified domain: {cimd_domain_escaped}
""" # Build redirect URI section (yellow box, centered) redirect_uri_escaped = html_module.escape(redirect_uri) redirect_section = f"""
Credentials will be sent to:
{redirect_uri_escaped}
""" # Build advanced details with collapsible section detail_rows = [ ("Application Name", html_module.escape(client_name or client_id)), ("Application Website", html_module.escape(client_website_url or "N/A")), ("Application ID", html_module.escape(client_id)), ("Redirect URI", redirect_uri_escaped), ( "Requested Scopes", ", ".join(html_module.escape(s) for s in scopes) if scopes else "None", ), ] detail_rows_html = "\n".join( [ f"""
{label}:
{value}
""" for label, value in detail_rows ] ) advanced_details = f"""
Advanced Details
{detail_rows_html}
""" # Build form with buttons # Use empty action to submit to current URL (/consent or /mcp/consent) # The POST handler is registered at the same path as GET form = f"""
""" # Build help link with tooltip (identical to current implementation) help_link = """ """ # Build the page content content = f"""
{create_logo(icon_url=server_icon_url, alt_text=server_name or "FastMCP")}

Application Access Request

{intro_box} {cimd_badge} {redirect_section} {advanced_details} {form}
{help_link} """ # Additional styles needed for this page cimd_badge_styles = """ .cimd-badge { background: #ecfdf5; border: 1px solid #6ee7b7; border-radius: 8px; padding: 8px 16px; margin-bottom: 16px; font-size: 14px; color: #065f46; text-align: center; } .cimd-check { color: #059669; font-weight: bold; margin-right: 4px; } """ additional_styles = ( INFO_BOX_STYLES + REDIRECT_SECTION_STYLES + DETAILS_STYLES + DETAIL_BOX_STYLES + BUTTON_STYLES + TOOLTIP_STYLES + cimd_badge_styles ) # Determine CSP policy to use # If csp_policy is None, build the default CSP policy # If csp_policy is empty string, CSP will be disabled entirely in create_page # If csp_policy is a non-empty string, use it as-is if csp_policy is None: # The consent form posts to itself (action="") and all subsequent redirects # are server-controlled. Chrome enforces form-action across the entire redirect # chain (Chromium issue #40923007), which breaks flows where an HTTPS callback # internally redirects to a custom scheme (e.g., claude:// or cursor://). # Since the form target is same-origin and we control the redirect chain, # omitting form-action is safe and avoids these browser-specific CSP issues. csp_policy = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'" return create_page( content=content, title=title, additional_styles=additional_styles, csp_policy=csp_policy, ) def create_error_html( error_title: str, error_message: str, error_details: dict[str, str] | None = None, server_name: str | None = None, server_icon_url: str | None = None, ) -> str: """Create a styled HTML error page for OAuth errors. Args: error_title: The error title (e.g., "OAuth Error", "Authorization Failed") error_message: The main error message to display error_details: Optional dictionary of error details to show (e.g., `{"Error Code": "invalid_client"}`) server_name: Optional server name to display server_icon_url: Optional URL to server icon/logo Returns: Complete HTML page as a string """ import html as html_module error_message_escaped = html_module.escape(error_message) # Build error message box error_box = f"""

{error_message_escaped}

""" # Build error details section if provided details_section = "" if error_details: detail_rows_html = "\n".join( [ f"""
{html_module.escape(label)}:
{html_module.escape(value)}
""" for label, value in error_details.items() ] ) details_section = f"""
Error Details
{detail_rows_html}
""" # Build the page content content = f"""
{create_logo(icon_url=server_icon_url, alt_text=server_name or "FastMCP")}

{html_module.escape(error_title)}

{error_box} {details_section}
""" # Additional styles needed for this page # Override .info-box.error to use normal text color instead of red additional_styles = ( INFO_BOX_STYLES + DETAILS_STYLES + DETAIL_BOX_STYLES + """ .info-box.error { color: #111827; } """ ) # Simple CSP policy for error pages (no forms needed) csp_policy = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'" return create_page( content=content, title=error_title, additional_styles=additional_styles, csp_policy=csp_policy, )