mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 21:44:18 +02:00
Redesign OAuth consent screen for better UX (#2170)
Implements a user-friendly consent screen design that reduces intimidation for non-technical users while maintaining security.
This commit is contained in:
parent
77ae1cf2b5
commit
83d77257da
2 changed files with 155 additions and 35 deletions
|
|
@ -68,9 +68,10 @@ from fastmcp.utilities.logging import get_logger
|
|||
from fastmcp.utilities.ui import (
|
||||
BUTTON_STYLES,
|
||||
DETAIL_BOX_STYLES,
|
||||
DETAILS_STYLES,
|
||||
INFO_BOX_STYLES,
|
||||
REDIRECT_SECTION_STYLES,
|
||||
TOOLTIP_STYLES,
|
||||
create_detail_box,
|
||||
create_logo,
|
||||
create_page,
|
||||
create_secure_html_response,
|
||||
|
|
@ -233,16 +234,13 @@ def create_consent_html(
|
|||
txn_id: str,
|
||||
csrf_token: str,
|
||||
client_name: str | None = None,
|
||||
title: str = "Authorization Consent",
|
||||
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,
|
||||
) -> str:
|
||||
"""Create a styled HTML consent page for OAuth authorization requests."""
|
||||
# Format scopes for display
|
||||
scopes_display = ", ".join(scopes) if scopes else "None"
|
||||
|
||||
# Build warning box with client name if available
|
||||
import html as html_module
|
||||
|
||||
client_display = html_module.escape(client_name or client_id)
|
||||
|
|
@ -251,29 +249,58 @@ def create_consent_html(
|
|||
# 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'<a href="{website_url_escaped}" target="_blank" rel="noopener noreferrer">{server_name_escaped}</a>'
|
||||
server_display = f'<a href="{website_url_escaped}" target="_blank" rel="noopener noreferrer" class="server-name-link">{server_name_escaped}</a>'
|
||||
else:
|
||||
server_display = server_name_escaped
|
||||
|
||||
warning_box = f"""
|
||||
<div class="warning-box">
|
||||
<p><strong>{client_display}</strong> is requesting access to <strong>{server_display}</strong>.</p>
|
||||
<p>Review the details below before approving.</p>
|
||||
# Build intro box with call-to-action
|
||||
intro_box = f"""
|
||||
<div class="info-box">
|
||||
<p>The application <strong>{client_display}</strong> wants to access the MCP server <strong>{server_display}</strong>. Please ensure you recognize the callback address below.</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# Build detail box with client information
|
||||
detail_rows = []
|
||||
if client_name:
|
||||
detail_rows.append(("Client Name", client_name))
|
||||
detail_rows.extend(
|
||||
# Build redirect URI section (yellow box, centered)
|
||||
redirect_uri_escaped = html_module.escape(redirect_uri)
|
||||
redirect_section = f"""
|
||||
<div class="redirect-section">
|
||||
<span class="label">Credentials will be sent to:</span>
|
||||
<div class="value">{redirect_uri_escaped}</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
# 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", 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(
|
||||
[
|
||||
("Client ID", client_id),
|
||||
("Redirect URI", redirect_uri),
|
||||
("Requested Scopes", scopes_display),
|
||||
f"""
|
||||
<div class="detail-row">
|
||||
<div class="detail-label">{label}:</div>
|
||||
<div class="detail-value">{value}</div>
|
||||
</div>
|
||||
"""
|
||||
for label, value in detail_rows
|
||||
]
|
||||
)
|
||||
detail_box = create_detail_box(detail_rows)
|
||||
|
||||
advanced_details = f"""
|
||||
<details>
|
||||
<summary>Advanced Details</summary>
|
||||
<div class="detail-box">
|
||||
{detail_rows_html}
|
||||
</div>
|
||||
</details>
|
||||
"""
|
||||
|
||||
# Build form with buttons
|
||||
form = f"""
|
||||
|
|
@ -281,13 +308,13 @@ def create_consent_html(
|
|||
<input type="hidden" name="txn_id" value="{txn_id}" />
|
||||
<input type="hidden" name="csrf_token" value="{csrf_token}" />
|
||||
<div class="button-group">
|
||||
<button type="submit" name="action" value="approve" class="btn-approve">Approve</button>
|
||||
<button type="submit" name="action" value="approve" class="btn-approve">Allow Access</button>
|
||||
<button type="submit" name="action" value="deny" class="btn-deny">Deny</button>
|
||||
</div>
|
||||
</form>
|
||||
"""
|
||||
|
||||
# Build help link with tooltip
|
||||
# Build help link with tooltip (identical to current implementation)
|
||||
help_link = """
|
||||
<div class="help-link-container">
|
||||
<span class="help-link">
|
||||
|
|
@ -312,9 +339,10 @@ def create_consent_html(
|
|||
content = f"""
|
||||
<div class="container">
|
||||
{create_logo(icon_url=server_icon_url, alt_text=server_name or "FastMCP")}
|
||||
<h1>Authorization Consent</h1>
|
||||
{warning_box}
|
||||
{detail_box}
|
||||
<h1>Application Access Request</h1>
|
||||
{intro_box}
|
||||
{redirect_section}
|
||||
{advanced_details}
|
||||
{form}
|
||||
</div>
|
||||
{help_link}
|
||||
|
|
@ -322,7 +350,12 @@ def create_consent_html(
|
|||
|
||||
# Additional styles needed for this page
|
||||
additional_styles = (
|
||||
INFO_BOX_STYLES + DETAIL_BOX_STYLES + BUTTON_STYLES + TOOLTIP_STYLES
|
||||
INFO_BOX_STYLES
|
||||
+ REDIRECT_SECTION_STYLES
|
||||
+ DETAILS_STYLES
|
||||
+ DETAIL_BOX_STYLES
|
||||
+ BUTTON_STYLES
|
||||
+ TOOLTIP_STYLES
|
||||
)
|
||||
|
||||
# Need to allow form-action for form submission
|
||||
|
|
|
|||
|
|
@ -111,15 +111,23 @@ BUTTON_STYLES = """
|
|||
# Info box / message box styles
|
||||
INFO_BOX_STYLES = """
|
||||
.info-box {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.875rem;
|
||||
margin: 1.25rem 0;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: left;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-box p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-box.centered {
|
||||
|
|
@ -133,10 +141,22 @@ INFO_BOX_STYLES = """
|
|||
}
|
||||
|
||||
.info-box strong {
|
||||
color: #111827;
|
||||
color: #0ea5e9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.info-box .server-name-link {
|
||||
color: #0ea5e9;
|
||||
text-decoration: underline;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.info-box .server-name-link:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
|
|
@ -236,10 +256,11 @@ DETAIL_BOX_STYLES = """
|
|||
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
min-width: 140px;
|
||||
min-width: 160px;
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
flex-shrink: 0;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
|
|
@ -252,6 +273,72 @@ DETAIL_BOX_STYLES = """
|
|||
}
|
||||
"""
|
||||
|
||||
# Redirect section styles (for OAuth redirect URI box)
|
||||
REDIRECT_SECTION_STYLES = """
|
||||
.redirect-section {
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fcd34d;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.redirect-section .label {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.redirect-section .value {
|
||||
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
color: #111827;
|
||||
word-break: break-all;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
"""
|
||||
|
||||
# Collapsible details styles
|
||||
DETAILS_STYLES = """
|
||||
details {
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
list-style: none;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
summary:hover {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
summary::before {
|
||||
content: "▶";
|
||||
display: inline-block;
|
||||
margin-right: 0.5rem;
|
||||
transition: transform 0.2s;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
details[open] summary::before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
"""
|
||||
|
||||
# Helper text styles
|
||||
HELPER_TEXT_STYLES = """
|
||||
.close-instruction, .help-text {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue