Support custom server name, icons, and link in OAuth Proxy consent page (#2135)

* Implement icon support in fastmcp

* Fix icon feature tests

- Update snapshot for ResourceTemplate to include icons field
- Remove OAuth mounting tests (belong to PR #2119, not this feature)

* Update docs

* Customize consent screen

* Use server website link if available

* Anchor link shouldnt have trailing slash

* Remove 'a FastMCP server named' from consent page message

* Update docs
This commit is contained in:
Jeremiah Lowin 2025-10-18 11:49:26 -04:00 committed by GitHub
commit 9e78d755c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1000 additions and 12 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 KiB

View file

@ -488,6 +488,12 @@ The OAuth proxy works by bridging DCR clients to traditional auth providers, whi
FastMCP's OAuth proxy requires you to explicitly consent whenever any new or unrecognized client attempts to connect to your server. Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. Once you approve a client, it's remembered so you don't see the consent page again for that client. The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.
![](/assets/images/oauth-proxy-consent-screen.png)
The consent page automatically displays your server's name, icon, and website URL, if available. These visual identifiers help users confirm they're authorizing the correct server.
**Learn more:**
- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance
- [Confused Deputy Attacks Explained](https://den.dev/blog/mcp-confused-deputy-api-management/) - Detailed walkthrough by Den Delimarsky

View file

@ -22,7 +22,16 @@ auth = GitHubProvider(
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
mcp = FastMCP("GitHub OAuth Example Server", auth=auth)
mcp = FastMCP(
auth=auth,
# "GitHub OAuth Example Server",
# icons=[
# Icon(
# src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/Octicons-mark-github.svg/2048px-Octicons-mark-github.svg.png"
# )
# ],
# website_url="https://example.com",
)
@mcp.tool

View file

@ -234,16 +234,30 @@ def create_consent_html(
csrf_token: str,
client_name: str | None = None,
title: str = "Authorization Consent",
server_name: str | None = None,
server_icon_url: str | None = None,
server_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
client_display = client_name or client_id
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'<a href="{website_url_escaped}" target="_blank" rel="noopener noreferrer">{server_name_escaped}</a>'
else:
server_display = server_name_escaped
warning_box = f"""
<div class="warning-box">
<p><strong>{client_display} is requesting access to this FastMCP server.</strong></p>
<p><strong>{client_display}</strong> is requesting access to <strong>{server_display}</strong>.</p>
<p>Review the details below before approving.</p>
</div>
"""
@ -286,7 +300,7 @@ def create_consent_html(
attacks</a>, where malicious clients could impersonate you
and steal access.<br><br>
<a
href="https://gofastmcp.com/servers/auth/oauth-proxy#confused-deputy-attacks/"
href="https://gofastmcp.com/servers/auth/oauth-proxy#confused-deputy-attacks"
target="_blank" class="tooltip-link">Learn more about
FastMCP security </a>
</span>
@ -297,7 +311,7 @@ def create_consent_html(
# Build the page content
content = f"""
<div class="container">
{create_logo()}
{create_logo(icon_url=server_icon_url, alt_text=server_name or "FastMCP")}
<h1>Authorization Consent</h1>
{warning_box}
{detail_box}
@ -1843,6 +1857,8 @@ class OAuthProxy(OAuthProvider):
self, request: Request
) -> HTMLResponse | RedirectResponse:
"""Display consent page or auto-approve/deny based on cookies."""
from fastmcp.server.server import FastMCP
txn_id = request.query_params.get("txn_id")
if not txn_id:
return create_secure_html_response(
@ -1895,6 +1911,19 @@ class OAuthProxy(OAuthProvider):
client = await self.get_client(txn["client_id"])
client_name = getattr(client, "client_name", None) if client else None
# Extract server metadata from app state
fastmcp = getattr(request.app.state, "fastmcp_server", None)
if isinstance(fastmcp, FastMCP):
server_name = fastmcp.name
icons = fastmcp.icons
server_icon_url = icons[0].src if icons else None
server_website_url = fastmcp.website_url
else:
server_name = None
server_icon_url = None
server_website_url = None
html = create_consent_html(
client_id=txn["client_id"],
redirect_uri=txn["client_redirect_uri"],
@ -1902,6 +1931,9 @@ class OAuthProxy(OAuthProvider):
txn_id=txn_id,
csrf_token=csrf_token,
client_name=client_name,
server_name=server_name,
server_icon_url=server_icon_url,
server_website_url=server_website_url,
)
response = create_secure_html_response(html)
# Store CSRF in cookie with short lifetime

View file

@ -346,6 +346,17 @@ class FastMCP(Generic[LifespanResultT]):
def version(self) -> str | None:
return self._mcp_server.version
@property
def website_url(self) -> str | None:
return self._mcp_server.website_url
@property
def icons(self) -> list[mcp.types.Icon]:
if self._mcp_server.icons is None:
return []
else:
return list(self._mcp_server.icons)
@asynccontextmanager
async def _lifespan_manager(self) -> AsyncIterator[None]:
if self._lifespan_result_set:

View file

@ -138,18 +138,18 @@ INFO_BOX_STYLES = """
}
.warning-box {
background: #fffbeb;
border: 1px solid #fcd34d;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: left;
text-align: center;
}
.warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #92400e;
color: #6b7280;
font-size: 0.9375rem;
}
@ -158,8 +158,20 @@ INFO_BOX_STYLES = """
}
.warning-box strong {
color: #0ea5e9;
font-weight: 600;
}
.warning-box a {
color: #0ea5e9;
text-decoration: underline;
font-weight: 600;
}
.warning-box a:hover {
color: #0284c7;
text-decoration: underline;
}
"""
# Status message styles (for success/error indicators)
@ -362,9 +374,19 @@ def create_page(
"""
def create_logo() -> str:
"""Create FastMCP logo HTML."""
return f'<img src="{FASTMCP_LOGO_URL}" alt="FastMCP" class="logo" />'
def create_logo(icon_url: str | None = None, alt_text: str = "FastMCP") -> str:
"""Create logo HTML.
Args:
icon_url: Optional custom icon URL. If not provided, uses the FastMCP logo.
alt_text: Alt text for the logo image.
Returns:
HTML for logo image tag.
"""
url = icon_url or FASTMCP_LOGO_URL
alt = html.escape(alt_text)
return f'<img src="{html.escape(url)}" alt="{alt}" class="logo" />'
def create_status_message(message: str, is_success: bool = True) -> str:

View file

@ -657,3 +657,214 @@ class TestConsentSecurity:
assert r2.headers.get("location", "").startswith(
"https://github.com/login/oauth/authorize"
)
class TestConsentPageServerIcon:
"""Tests for server icon display in OAuth consent screen."""
async def test_consent_screen_displays_server_icon(self):
"""Test that consent screen shows server's custom icon when available."""
from unittest.mock import Mock
from fastmcp import FastMCP
# Create mock JWT verifier
verifier = Mock(spec=TokenVerifier)
verifier.required_scopes = ["read"]
verifier.verify_token = Mock(return_value=None)
# Create OAuthProxy
proxy = OAuthProxy(
upstream_authorization_endpoint="https://oauth.example.com/authorize",
upstream_token_endpoint="https://oauth.example.com/token",
upstream_client_id="upstream-client",
upstream_client_secret="upstream-secret",
token_verifier=verifier,
base_url="https://proxy.example.com",
)
# Create FastMCP server with custom icon
from mcp.types import Icon
server = FastMCP(
name="My Custom Server",
auth=proxy,
icons=[Icon(src="https://example.com/custom-icon.png")],
website_url="https://example.com",
)
# Create HTTP app
app = server.http_app()
# Register a test client with the proxy
client_info = OAuthClientInformationFull(
client_id="test-client",
client_secret="test-secret",
redirect_uris=[AnyUrl("http://localhost:12345/callback")],
)
await proxy.register_client(client_info)
# Create a transaction manually
from fastmcp.server.auth.oauth_proxy import OAuthTransaction
txn_id = "test-txn-id"
transaction = OAuthTransaction(
txn_id=txn_id,
client_id="test-client",
client_redirect_uri="http://localhost:12345/callback",
client_state="client-state",
code_challenge="challenge",
code_challenge_method="S256",
scopes=["read"],
created_at=time.time(),
)
await proxy._transaction_store.put(key=txn_id, value=transaction)
# Make request to consent page
with TestClient(app) as client:
response = client.get(f"/consent?txn_id={txn_id}")
# Check that response is successful
assert response.status_code == 200
# Check that HTML contains custom icon
assert "https://example.com/custom-icon.png" in response.text
# Check that server name is used as alt text
assert 'alt="My Custom Server"' in response.text
async def test_consent_screen_falls_back_to_fastmcp_logo(self):
"""Test that consent screen shows FastMCP logo when no server icon provided."""
from unittest.mock import Mock
from fastmcp import FastMCP
# Create mock JWT verifier
verifier = Mock(spec=TokenVerifier)
verifier.required_scopes = ["read"]
verifier.verify_token = Mock(return_value=None)
# Create OAuthProxy
proxy = OAuthProxy(
upstream_authorization_endpoint="https://oauth.example.com/authorize",
upstream_token_endpoint="https://oauth.example.com/token",
upstream_client_id="upstream-client",
upstream_client_secret="upstream-secret",
token_verifier=verifier,
base_url="https://proxy.example.com",
)
# Create FastMCP server without icon
server = FastMCP(name="Server Without Icon", auth=proxy)
# Create HTTP app
app = server.http_app()
# Register a test client
client_info = OAuthClientInformationFull(
client_id="test-client",
client_secret="test-secret",
redirect_uris=[AnyUrl("http://localhost:12345/callback")],
)
await proxy.register_client(client_info)
# Create a transaction
from fastmcp.server.auth.oauth_proxy import OAuthTransaction
txn_id = "test-txn-id"
transaction = OAuthTransaction(
txn_id=txn_id,
client_id="test-client",
client_redirect_uri="http://localhost:12345/callback",
client_state="client-state",
code_challenge="challenge",
code_challenge_method="S256",
scopes=["read"],
created_at=time.time(),
)
await proxy._transaction_store.put(key=txn_id, value=transaction)
# Make request to consent page
with TestClient(app) as client:
response = client.get(f"/consent?txn_id={txn_id}")
# Check that response is successful
assert response.status_code == 200
# Check that HTML contains FastMCP logo
assert "gofastmcp.com/assets/brand/blue-logo.png" in response.text
# Check that alt text is still the server name
assert 'alt="Server Without Icon"' in response.text
async def test_consent_screen_escapes_server_name(self):
"""Test that server name is properly HTML-escaped."""
from unittest.mock import Mock
from mcp.types import Icon
from fastmcp import FastMCP
# Create mock JWT verifier
verifier = Mock(spec=TokenVerifier)
verifier.required_scopes = ["read"]
verifier.verify_token = Mock(return_value=None)
# Create OAuthProxy
proxy = OAuthProxy(
upstream_authorization_endpoint="https://oauth.example.com/authorize",
upstream_token_endpoint="https://oauth.example.com/token",
upstream_client_id="upstream-client",
upstream_client_secret="upstream-secret",
token_verifier=verifier,
base_url="https://proxy.example.com",
)
# Create FastMCP server with special characters in name
server = FastMCP(
name='<script>alert("xss")</script>Server',
auth=proxy,
icons=[Icon(src="https://example.com/icon.png")],
)
# Create HTTP app
app = server.http_app()
# Register a test client
client_info = OAuthClientInformationFull(
client_id="test-client",
client_secret="test-secret",
redirect_uris=[AnyUrl("http://localhost:12345/callback")],
)
await proxy.register_client(client_info)
# Create a transaction
from fastmcp.server.auth.oauth_proxy import OAuthTransaction
txn_id = "test-txn-id"
transaction = OAuthTransaction(
txn_id=txn_id,
client_id="test-client",
client_redirect_uri="http://localhost:12345/callback",
client_state="client-state",
code_challenge="challenge",
code_challenge_method="S256",
scopes=["read"],
created_at=time.time(),
)
await proxy._transaction_store.put(key=txn_id, value=transaction)
# Make request to consent page
with TestClient(app) as client:
response = client.get(f"/consent?txn_id={txn_id}")
# Check that response is successful
assert response.status_code == 200
# Check that script tag is escaped
assert "<script>" not in response.text
assert "&lt;script&gt;" in response.text
assert (
'alt="&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;Server"'
in response.text
)

697
warning_box_test.html Normal file
View file

@ -0,0 +1,697 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warning Box Design Options</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f9fafb;
padding: 2rem;
color: #111827;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
margin-bottom: 2rem;
font-size: 2rem;
}
.options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: 2rem;
}
.option {
background: white;
padding: 2rem;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.option h2 {
font-size: 0.875rem;
font-weight: 600;
color: #6b7280;
margin-bottom: 1.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Option 1: Original yellow box (improved) */
.option1 .warning-box {
background: #fffbeb;
border: 1px solid #fde047;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option1 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #ca8a04;
font-size: 0.9375rem;
}
.option1 .warning-box a {
color: #ca8a04;
text-decoration: underline;
font-weight: 600;
}
/* Option 2: Yellow with darker text */
.option2 .warning-box {
background: #fffbeb;
border: 1px solid #fde047;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option2 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #111827;
font-size: 0.9375rem;
}
.option2 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 3: Left border accent */
.option3 .warning-box {
background: transparent;
border: none;
border-left: 4px solid #fbbf24;
padding: 1rem;
padding-left: 1.25rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option3 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #111827;
font-size: 0.9375rem;
}
.option3 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 4: Very subtle background */
.option4 .warning-box {
background: #fef9e7;
border: none;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option4 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #111827;
font-size: 0.9375rem;
}
.option4 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 5: Top accent bar */
.option5 .warning-box {
background: #fffbeb;
border: 1px solid #fde047;
border-top: 3px solid #fbbf24;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option5 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #111827;
font-size: 0.9375rem;
}
.option5 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 6: All yellow but darker text */
.option6 .warning-box {
background: #fffbeb;
border: 1px solid #fde047;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option6 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #92400e;
font-size: 0.9375rem;
}
.option6 .warning-box a {
color: #92400e;
text-decoration: underline;
font-weight: 600;
}
/* Option 7: Minimal - just link styling */
.option7 .warning-box {
background: transparent;
border: none;
padding: 1rem 0;
margin-bottom: 1.5rem;
text-align: center;
}
.option7 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #111827;
font-size: 0.9375rem;
}
.option7 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 8: Icon + yellow */
.option8 .warning-box {
background: #fffbeb;
border: 1px solid #fde047;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option8 .warning-box::before {
content: "⚠️ ";
margin-right: 0.5rem;
}
.option8 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #111827;
font-size: 0.9375rem;
}
.option8 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 9: Yellow, dark gray text */
.option9 .warning-box {
background: #fffbeb;
border: 1px solid #fde047;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option9 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #5a6c7d;
font-size: 0.9375rem;
}
.option9 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 10: Yellow, charcoal text */
.option10 .warning-box {
background: #fffbeb;
border: 1px solid #fde047;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option10 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #0f172a;
font-size: 0.9375rem;
}
.option10 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 11: Blue background, dark text */
.option11 .warning-box {
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option11 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #1f2937;
font-size: 0.9375rem;
}
.option11 .warning-box a {
color: #2563eb;
text-decoration: underline;
font-weight: 600;
}
/* Option 12: Richer blue, dark text */
.option12 .warning-box {
background: #dbeafe;
border: 1px solid #60a5fa;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option12 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #1f2937;
font-size: 0.9375rem;
}
.option12 .warning-box a {
color: #2563eb;
text-decoration: underline;
font-weight: 600;
}
/* Option 13: Light amber, neutral dark text */
.option13 .warning-box {
background: #fef3c7;
border: 1px solid #fcd34d;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option13 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #1f2937;
font-size: 0.9375rem;
}
.option13 .warning-box a {
color: #ca8a04;
text-decoration: underline;
font-weight: 600;
}
/* Option 14: Pale orange, dark text */
.option14 .warning-box {
background: #ffedd5;
border: 1px solid #fed7aa;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option14 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #1f2937;
font-size: 0.9375rem;
}
.option14 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 15: Light green, dark text */
.option15 .warning-box {
background: #f0fdf4;
border: 1px solid #86efac;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option15 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #0f172a;
font-size: 0.9375rem;
}
.option15 .warning-box a {
color: #16a34a;
text-decoration: underline;
font-weight: 600;
}
/* Option 16: Light orange, dark text */
.option16 .warning-box {
background: #fff7ed;
border: 1px solid #fed7aa;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option16 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #0f172a;
font-size: 0.9375rem;
}
.option16 .warning-box a {
color: #d97706;
text-decoration: underline;
font-weight: 600;
}
/* Option 17: Light gray, dark text */
.option17 .warning-box {
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option17 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #0f172a;
font-size: 0.9375rem;
}
.option17 .warning-box a {
color: #6b7280;
text-decoration: underline;
font-weight: 600;
}
/* Option 18: Brighter blue, dark text */
.option18 .warning-box {
background: #e0f2fe;
border: 1px solid #7dd3fc;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option18 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #0f172a;
font-size: 0.9375rem;
}
.option18 .warning-box a {
color: #0369a1;
text-decoration: underline;
font-weight: 600;
}
/* Option 19: Light green with green strong, softer gray text */
.option19 .warning-box {
background: #f0fdf4;
border: 1px solid #86efac;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option19 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #4b5563;
font-size: 0.9375rem;
}
.option19 .warning-box strong {
color: #16a34a;
font-weight: 600;
}
.option19 .warning-box a {
color: #16a34a;
text-decoration: underline;
font-weight: 600;
}
/* Option 20: Tailwind sky palette */
.option20 .warning-box {
background: #f0f9ff;
border: 1px solid #7dd3fc;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.option20 .warning-box p {
margin-bottom: 0.5rem;
line-height: 1.5;
color: #475569;
font-size: 0.9375rem;
}
.option20 .warning-box strong {
color: #0ea5e9;
font-weight: 600;
}
.option20 .warning-box a {
color: #0ea5e9;
text-decoration: underline;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<h1>Warning Box Design Options</h1>
<div class="options">
<div class="option option1">
<h2>Option 1: Original Yellow with Gold Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option2">
<h2>Option 2: Yellow Box, Dark Text, Orange Link</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option3">
<h2>Option 3: Left Border Only</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option4">
<h2>Option 4: Very Subtle Background</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option5">
<h2>Option 5: Top Accent Bar</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option6">
<h2>Option 6: Yellow with Dark Brown Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option7">
<h2>Option 7: Minimal (No Box)</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option8">
<h2>Option 8: Yellow Box with Icon</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option9">
<h2>Option 9: Yellow, Dark Gray Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option10">
<h2>Option 10: Yellow, Charcoal Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option11">
<h2>Option 11: Light Blue, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option12">
<h2>Option 12: Richer Blue, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option13">
<h2>Option 13: Light Amber, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option14">
<h2>Option 14: Pale Orange, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option15">
<h2>Option 15: Light Green, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option16">
<h2>Option 16: Light Orange, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option17">
<h2>Option 17: Light Gray, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option18">
<h2>Option 18: Brighter Blue, Dark Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option19">
<h2>Option 19: Green Box, Green Strong, Softer Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
<div class="option option20">
<h2>Option 20: Blue Box, Blue Strong, Softer Text</h2>
<div class="warning-box">
<p><strong>FastMCP Client</strong> is requesting access to a FastMCP server named <a href="#">FastMCP-6d8c</a>.</p>
<p>Review the details below before approving.</p>
</div>
</div>
</div>
</div>
</body>
</html>