mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
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:
parent
5ce67f756b
commit
9e78d755c2
8 changed files with 1000 additions and 12 deletions
BIN
docs/assets/images/oauth-proxy-consent-screen.png
Normal file
BIN
docs/assets/images/oauth-proxy-consent-screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 607 KiB |
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 "<script>" in response.text
|
||||
assert (
|
||||
'alt="<script>alert("xss")</script>Server"'
|
||||
in response.text
|
||||
)
|
||||
|
|
|
|||
697
warning_box_test.html
Normal file
697
warning_box_test.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue