feat(mcp): browser hello page with working middleware and config-driven content#40471
Conversation
When a browser opens the MCP endpoint (Accept: text/html without application/json or text/event-stream), return a 200 HTML page explaining what the endpoint is and how to configure it in Claude Desktop, Claude Code, or Cursor. API and SSE clients continue to receive the existing JSON 401 response unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…HEAD - Introduce MCPJWTVerifier(JWTVerifier) base class that registers _auth_error_handler as the Starlette on_error callback; previously the callback was only wired inside DetailedJWTVerifier (MCP_JWT_DEBUG_ERRORS=True), so the HTML page was never shown in the default configuration - mcp_config.py non-debug path now uses MCPJWTVerifier instead of bare JWTVerifier; DetailedJWTVerifier inherits MCPJWTVerifier - Add _prefers_browser_html() helper: checks method (GET/HEAD only) and Accept header (case-insensitive); prevents POST/OPTIONS with text/html from incorrectly receiving a 200 HTML response - Rename _json_auth_error_handler -> _auth_error_handler, return type narrowed to Response (Starlette base class, matching on_error signature) - Add tests: POST+text/html -> 401, HEAD+text/html -> 200, uppercase Accept Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PR renamed _json_auth_error_handler to _auth_error_handler in jwt_verifier.py (to reflect that it now returns HTML for browsers rather than always JSON), but test_jwt_verifier.py still imported the old name, causing a collection-time ImportError that failed all unit tests.
…ig-driven page - Replace on_error hook approach (dead code — BearerAuthBackend never raises AuthenticationError) with a Starlette BaseHTTPMiddleware that intercepts browser GET/HEAD requests before FastMCP's router returns 405 - Works regardless of MCP_AUTH_ENABLED — no longer requires auth to be on - Auth-aware: omits Authorization header from config snippet when auth is off - Config-driven via MCP_HELLO_PAGE dict: override title, server_key, show_transport, and clients list per deployment
- Escape server_key via superset.utils.json.dumps() to prevent invalid JSON in the config snippet when the key contains quotes or backslashes - HTML-escape title and client names to prevent XSS from crafted MCP_HELLO_PAGE config - Restrict BrowserHelloMiddleware to the MCP path only (default /mcp) so unrelated routes return their real 404/405 instead of the hello page - Derive auth_enabled from the actual auth provider returned by _create_auth_provider() so MCP_AUTH_FACTORY deployments show the correct Authorization header even when MCP_AUTH_ENABLED is False
Derive title, server_key, and description app name from APP_NAME config. Include logo from APP_ICON if it is an absolute URL. All values are overridable per-deploy via MCP_HELLO_PAGE config dict.
…SET_WEBSERVER_ADDRESS Root "/" now also triggers the browser hello page, not just "/mcp". Logo URL is constructed from SUPERSET_WEBSERVER_ADDRESS + APP_ICON when APP_ICON is a relative path (the Superset default).
BaseHTTPMiddleware only runs for routes FastMCP registers (/mcp), so the path check was never triggered for /. Remove the path restriction and rely on the Accept header check alone — MCP/API clients never send Accept: text/html so they are unaffected.
Code Review Agent Run #5fddbaActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
|
The PR comment indicates a logic error where the MCP hello page's authentication hint is derived from Flask config flags instead of the actual runtime auth configuration. This can lead to incorrect instructions for users regarding the inclusion of Authorization headers. To resolve this, the |
…not config flags MCP_AUTH_FACTORY may return None (auth disabled at runtime despite the factory being configured). use_factory_config auth lives in factory_config, not Flask config at all. Both cases produced a wrong auth hint on the hello page. Fix: set auth_provider in both run_server branches (factory path reads factory_config["auth"]) and drop the config-flag fallback from _build_starlette_middleware.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #40471 +/- ##
==========================================
- Coverage 64.18% 64.13% -0.06%
==========================================
Files 2592 2592
Lines 139257 139381 +124
Branches 32335 32361 +26
==========================================
+ Hits 89385 89392 +7
- Misses 48337 48452 +115
- Partials 1535 1537 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a browser-friendly “hello” HTML page for Superset’s MCP HTTP endpoint so browser navigations to /mcp (or /) show setup instructions instead of a 405 Method Not Allowed, while preserving RFC 6750-compliant JSON 401 behavior for API clients.
Changes:
- Introduces
BrowserHelloMiddleware(StarletteBaseHTTPMiddleware) to intercept browser-likeGET/HEADrequests and return a styled HTML setup page. - Updates the default MCP auth factory to use a Superset-specific
MCPJWTVerifier/_auth_error_handlerthat can return HTML for browser navigations and JSON401otherwise. - Adds unit tests validating content negotiation behavior for
_auth_error_handler.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
superset/mcp_service/jwt_verifier.py |
Adds HTML hello page generation, browser detection logic, middleware, and new auth error handler / verifier base class. |
superset/mcp_service/server.py |
Wires the new Starlette middleware into both mcp.run() and mcp.http_app() paths; derives page config from Flask config. |
superset/mcp_service/mcp_config.py |
Switches the default non-debug auth provider to Superset’s MCPJWTVerifier to enable the new handler/page. |
tests/unit_tests/mcp_service/test_jwt_verifier.py |
Updates existing tests to reference _auth_error_handler instead of the removed _json_auth_error_handler. |
tests/unit_tests/mcp_service/test_jwt_verifier_browser_hello.py |
Adds unit tests for HTML-vs-JSON behavior based on Accept and HTTP method. |
Code Review Agent Run #20d94fActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
- XSS: html.escape() the entire config snippet before embedding in HTML;
json.dumps() does not escape < / > so a crafted server_key could break
out of the <pre><code> block. Use raw <> in _build_config_snippet and
escape the whole string at the call site.
- TypeError: validate MCP_HELLO_PAGE is a dict before merging; log a
warning and fall back to {} when it is an unexpected type.
|
Bito Automatic Review Skipped – PR Already Merged |
The browser-friendly hello page (apache#40471) re-added MCPJWTVerifier to jwt_verifier.py. Update _build_jwt_verifier to use MCPJWTVerifier for the non-debug path, matching the current jwt_verifier.py interface. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SUMMARY
Adds a browser-friendly hello page for the MCP endpoint. When a user opens the MCP URL in a browser, they see a styled HTML page with setup instructions instead of a
405 Method Not Allowed.Implementation:
BrowserHelloMiddleware(BaseHTTPMiddleware) interceptsGET/HEADrequests with a browserAccept: text/htmlheader before they reach FastMCP's router. Wired inserver.pyfor both single-pod (mcp.run()) and multi-pod (mcp.http_app()+ uvicorn) paths.Features:
Authorizationheader when auth is off, includes it when auth is onAPP_NAME; logo shown ifAPP_ICONis an absolute URL or ifSUPERSET_WEBSERVER_ADDRESSis set (used to resolve relative icon paths)MCP_HELLO_PAGEdict — deployments can overridetitle,server_key,show_transport,clients,logo_url, andapp_nameBEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
Before:
405 Method Not Allowedfor any browserGETto/mcpAfter: 200 HTML page with setup instructions. Config snippet adapts to auth state:
{"url": "<this-url>", "transport": "streamable-http"}"Authorization": "Bearer <your-api-key>"TESTING INSTRUCTIONS
superset mcp run --port 5008 --debughttp://localhost:5008/mcp— should show the HTML hello pagehttp://localhost:5008/— should also show the hello pagecurl -s -H "Accept: text/html,*/*" http://localhost:5008/mcp— should return 200 HTMLcurl -s -H "Accept: application/json" http://localhost:5008/mcp— should return 405 (unaffected)MCP_AUTH_ENABLED = Truein config, restart — config snippet should includeAuthorizationheaderMCP_HELLO_PAGE = {"title": "My MCP", "server_key": "my-superset"}— page should reflect overridesADDITIONAL INFORMATION