From 00c458528687461e8434f9d2c7f63812d5f93b91 Mon Sep 17 00:00:00 2001
From: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com>
Date: Mon, 21 Sep 2026 14:16:24 -0700
Subject: [PATCH] fix(mcp): style provider-neutral connection consent
---
.../mcp_server/gateway_dcr_flow.py | 25 ++--
.../html_forms/native_client_consent.py | 130 ++++++++++++++----
.../mcp_server/test_discoverable_endpoints.py | 41 ++++++
.../html_forms/test_native_client_consent.py | 50 ++++++-
4 files changed, 204 insertions(+), 42 deletions(-)
diff --git a/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py b/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py
index bac15f0de98..8ec32ce88ed 100644
--- a/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py
+++ b/litellm/proxy/_experimental/mcp_server/gateway_dcr_flow.py
@@ -98,6 +98,7 @@ from litellm.proxy.common_utils.encrypt_decrypt_utils import (
encrypt_value_helper,
)
from litellm.proxy.common_utils.html_forms.native_client_consent import (
+ render_mcp_connection_consent_page,
render_native_client_consent_page,
)
from litellm.types.mcp_server.mcp_server_manager import MCPServer
@@ -1668,8 +1669,6 @@ async def authorize_connection(
resource: str | None,
scope: str | None,
) -> Response:
- from html import escape
-
rejected: Final = _rejected_authorize_request(
client_id, redirect_uri, state, code_challenge, code_challenge_method, response_type
)
@@ -1700,23 +1699,23 @@ async def authorize_connection(
exp=int(datetime.now(timezone.utc).timestamp()) + CONNECT_FLOW_TTL_SECONDS,
)
action: Final = f"{get_request_base_url(request)}/authorize/connection/complete"
+ style_nonce: Final = secrets.token_urlsafe(18)
response: Final = HTMLResponse(
- ''
- '
Authorize MCP connection'
- "Authorize MCP connection
"
- f"{escape(client.client_name or 'MCP client')} wants access to "
- f"{escape(server.name)}.
Return address: {escape(redirect_uri)}
"
- f"Requested permissions: {escape(scopes or 'provider defaults')}
"
- "Only approve if you started this connection. You will continue to the provider to sign in.
"
- f'",
+ render_mcp_connection_consent_page(
+ client_name=client.client_name or "MCP client",
+ server_name=server.name,
+ scopes=tuple(scopes.split()),
+ redirect_uri=redirect_uri,
+ flow_handle=handle,
+ complete_url=action,
+ style_nonce=style_nonce,
+ ),
headers=MappingProxyType(
{
**TOKEN_NO_CACHE_HEADERS,
"Referrer-Policy": "no-referrer",
"X-Frame-Options": "DENY",
- "Content-Security-Policy": "default-src 'none'; frame-ancestors 'none'",
+ "Content-Security-Policy": f"default-src 'none'; style-src 'nonce-{style_nonce}'; frame-ancestors 'none'",
}
),
)
diff --git a/litellm/proxy/common_utils/html_forms/native_client_consent.py b/litellm/proxy/common_utils/html_forms/native_client_consent.py
index dac92c4e787..6524f6a83cf 100644
--- a/litellm/proxy/common_utils/html_forms/native_client_consent.py
+++ b/litellm/proxy/common_utils/html_forms/native_client_consent.py
@@ -4,6 +4,37 @@ from typing import Final
from litellm.constants import CLI_JWT_EXPIRATION_HOURS
+_CONSENT_STYLES: Final = """
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background-color: #f8fafc;
+ margin: 0;
+ padding: 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ color: #1e293b;
+}
+.container {
+ background-color: #fff;
+ padding: 40px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ width: 450px;
+ max-width: 100%;
+}
+h1 { margin: 0 0 16px; font-size: 24px; font-weight: 600; }
+p { margin: 0 0 12px; line-height: 1.5; }
+code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; }
+label { display: block; margin: 16px 0 6px; font-weight: 600; }
+select { width: 100%; padding: 8px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; }
+.actions { display: flex; gap: 12px; margin-top: 24px; }
+button { flex: 1; padding: 10px; border-radius: 6px; font-size: 15px; cursor: pointer; border: 1px solid #cbd5e1; }
+.approve { background: #2563eb; color: #fff; border-color: #2563eb; }
+.deny { background: #fff; color: #1e293b; }
+"""
+
def render_native_client_consent_page(
*,
@@ -25,34 +56,7 @@ def render_native_client_consent_page(
Authorize CLI access - LiteLLM
@@ -89,3 +93,73 @@ def _team_field(teams: Sequence[tuple[str, str]]) -> str:
return (
f''
)
+
+
+def render_mcp_connection_consent_page(
+ *,
+ client_name: str,
+ server_name: str,
+ scopes: Sequence[str],
+ redirect_uri: str,
+ flow_handle: str,
+ complete_url: str,
+ style_nonce: str,
+) -> str:
+ permissions: Final = (
+ '' + "".join(f"{escape(scope)} " for scope in scopes) + "
"
+ if scopes
+ else 'Default permissions configured by the server.
'
+ )
+ return f"""
+
+
+
+
+
+Connect to {escape(server_name)} - LiteLLM
+
+
+
+
+LiteLLM
+Connect to {escape(server_name)}
+{escape(client_name)} is requesting access through LiteLLM.
+
+Requested permissions
+{permissions}
+
+
+Return to client
+{escape(redirect_uri)}
+
+Only continue if you started this connection and recognize the client and return address.
+
+Next, authorize access on the server's sign-in page.
+
+
+
+"""
diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py
index fbe82f07674..1d980746357 100644
--- a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py
+++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py
@@ -12422,6 +12422,7 @@ def keyed_oauth_client(monkeypatch):
def _start_keyed_oauth(harness):
+ import re
from urllib.parse import parse_qs, urlparse
client = harness.client
@@ -12461,6 +12462,13 @@ def _start_keyed_oauth(harness):
assert "http://localhost:33418/callback" in consent.text
assert "read:user" in consent.text
assert "/sso/" not in consent.text
+ style_nonce = re.search(r'