"""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"""
"""
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"""
"""
# 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,
)