diff --git a/docs/assets/images/oauth-proxy-consent-screen.png b/docs/assets/images/oauth-proxy-consent-screen.png
new file mode 100644
index 000000000..5613b66d9
Binary files /dev/null and b/docs/assets/images/oauth-proxy-consent-screen.png differ
diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index ce99a6c81..17bcbd412 100644
--- a/docs/servers/auth/oauth-proxy.mdx
+++ b/docs/servers/auth/oauth-proxy.mdx
@@ -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
diff --git a/examples/auth/github_oauth/server.py b/examples/auth/github_oauth/server.py
index 1f88c6977..23e787439 100644
--- a/examples/auth/github_oauth/server.py
+++ b/examples/auth/github_oauth/server.py
@@ -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
diff --git a/src/fastmcp/server/auth/oauth_proxy.py b/src/fastmcp/server/auth/oauth_proxy.py
index 6377b695d..9729bcea2 100644
--- a/src/fastmcp/server/auth/oauth_proxy.py
+++ b/src/fastmcp/server/auth/oauth_proxy.py
@@ -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'{server_name_escaped}'
+ else:
+ server_display = server_name_escaped
+
warning_box = f"""
- {create_logo()}
+ {create_logo(icon_url=server_icon_url, alt_text=server_name or "FastMCP")}
Authorization Consent
{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
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 813831ba3..c758f64cb 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -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:
diff --git a/src/fastmcp/utilities/ui.py b/src/fastmcp/utilities/ui.py
index e5a8429a2..359a1840b 100644
--- a/src/fastmcp/utilities/ui.py
+++ b/src/fastmcp/utilities/ui.py
@@ -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'

'
+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'
})
'
def create_status_message(message: str, is_success: bool = True) -> str:
diff --git a/tests/server/auth/test_oauth_consent_flow.py b/tests/server/auth/test_oauth_consent_flow.py
index 45d3cdb34..648bcf990 100644
--- a/tests/server/auth/test_oauth_consent_flow.py
+++ b/tests/server/auth/test_oauth_consent_flow.py
@@ -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='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 "