This is an automated email from the ASF dual-hosted git repository.
arivero pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 6663709a23 fix(mcp): tools not listed when JWT auth is enabled (#37377)
6663709a23 is described below
commit 6663709a23afe96ee881f50ef0fd6a28c5dc1770
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Wed Jan 28 05:20:20 2026 -0500
fix(mcp): tools not listed when JWT auth is enabled (#37377)
---
superset/mcp_service/app.py | 69 +++++++++-----------
superset/mcp_service/mcp_config.py | 10 +--
tests/unit_tests/mcp_service/test_mcp_config.py | 85 +++++++++++++++----------
3 files changed, 86 insertions(+), 78 deletions(-)
diff --git a/superset/mcp_service/app.py b/superset/mcp_service/app.py
index 64730a54ca..4a8fc4e729 100644
--- a/superset/mcp_service/app.py
+++ b/superset/mcp_service/app.py
@@ -349,9 +349,8 @@ def init_fastmcp_server(
"""
Initialize and configure the FastMCP server.
- This function provides a way to create a custom FastMCP instance
- instead of using the default global one. If parameters are provided,
- a new instance will be created with those settings.
+ This function configures the global MCP instance (which has all tools
+ already registered) with auth, middleware, and other settings.
Args:
name: Server name (defaults to "{APP_NAME} MCP Server")
@@ -361,7 +360,7 @@ def init_fastmcp_server(
**kwargs: Additional FastMCP configuration
Returns:
- FastMCP instance (either the global one or a new custom one)
+ The global FastMCP instance configured with the provided settings
"""
# Read branding from Flask config's APP_NAME
from superset.mcp_service.flask_singleton import app as flask_app
@@ -377,38 +376,32 @@ def init_fastmcp_server(
if instructions is None:
instructions = get_default_instructions(branding)
- # If any custom parameters are provided, create a new instance
- custom_params_provided = any(
- [
- name != default_name,
- instructions != get_default_instructions(branding),
- auth is not None,
- lifespan is not None,
- tools is not None,
- include_tags is not None,
- exclude_tags is not None,
- config is not None,
- middleware is not None,
- kwargs,
- ]
- )
+ # Configure the global mcp instance with provided settings.
+ # Tools are already registered on this instance via @tool decorator
imports above.
+ # name and instructions are read-only properties that delegate to
_mcp_server
+ mcp._mcp_server.name = name
+ mcp._mcp_server.instructions = instructions
- if custom_params_provided:
- logger.info("Creating custom FastMCP instance with provided
configuration")
- return create_mcp_app(
- name=name,
- instructions=instructions,
- auth=auth,
- lifespan=lifespan,
- tools=tools,
- include_tags=include_tags,
- exclude_tags=exclude_tags,
- config=config,
- middleware=middleware,
- **kwargs,
- )
- else:
- # Use the default global instance
- logger.setLevel(logging.DEBUG)
- logger.info("Using default FastMCP instance - scaffold version without
auth")
- return mcp
+ if auth is not None:
+ mcp.auth = auth
+ logger.info("Authentication configured on MCP instance")
+
+ if middleware is not None:
+ for mw in middleware:
+ mcp.add_middleware(mw)
+ logger.info("Added %d middleware(s) to MCP instance", len(middleware))
+
+ if lifespan is not None:
+ mcp.lifespan = lifespan
+
+ if include_tags is not None:
+ mcp.include_tags = include_tags
+
+ if exclude_tags is not None:
+ mcp.exclude_tags = exclude_tags
+
+ # Apply any additional configuration
+ _apply_config(mcp, config)
+
+ logger.info("Configured FastMCP instance: %s (auth=%s)", name, auth is not
None)
+ return mcp
diff --git a/superset/mcp_service/mcp_config.py
b/superset/mcp_service/mcp_config.py
index 678ce78941..5e9432bb4b 100644
--- a/superset/mcp_service/mcp_config.py
+++ b/superset/mcp_service/mcp_config.py
@@ -180,21 +180,21 @@ def create_default_mcp_auth_factory(app: Flask) ->
Optional[Any]:
return None
try:
- from fastmcp.server.auth.providers.bearer import BearerAuthProvider
+ from fastmcp.server.auth.providers.jwt import JWTVerifier
# For HS256 (symmetric), use the secret as the public_key parameter
if app.config.get("MCP_JWT_ALGORITHM") == "HS256" and secret:
- auth_provider = BearerAuthProvider(
+ auth_provider = JWTVerifier(
public_key=secret, # HS256 uses secret as key
issuer=app.config.get("MCP_JWT_ISSUER"),
audience=app.config.get("MCP_JWT_AUDIENCE"),
algorithm="HS256",
required_scopes=app.config.get("MCP_REQUIRED_SCOPES", []),
)
- logger.info("Created BearerAuthProvider with HS256 secret")
+ logger.info("Created JWTVerifier with HS256 secret")
else:
# For RS256 (asymmetric), use public key or JWKS
- auth_provider = BearerAuthProvider(
+ auth_provider = JWTVerifier(
jwks_uri=jwks_uri,
public_key=public_key,
issuer=app.config.get("MCP_JWT_ISSUER"),
@@ -203,7 +203,7 @@ def create_default_mcp_auth_factory(app: Flask) ->
Optional[Any]:
required_scopes=app.config.get("MCP_REQUIRED_SCOPES", []),
)
logger.info(
- "Created BearerAuthProvider with jwks_uri=%s, public_key=%s",
+ "Created JWTVerifier with jwks_uri=%s, public_key=%s",
jwks_uri,
"***" if public_key else None,
)
diff --git a/tests/unit_tests/mcp_service/test_mcp_config.py
b/tests/unit_tests/mcp_service/test_mcp_config.py
index ddec44dffb..6b466706d0 100644
--- a/tests/unit_tests/mcp_service/test_mcp_config.py
+++ b/tests/unit_tests/mcp_service/test_mcp_config.py
@@ -66,19 +66,12 @@ def test_init_fastmcp_server_with_default_app_name():
"sys.modules",
{"superset.mcp_service.flask_singleton":
MagicMock(app=mock_flask_app)},
):
- with patch("superset.mcp_service.app.create_mcp_app") as mock_create:
- mock_mcp = MagicMock()
- mock_create.return_value = mock_mcp
+ with patch("superset.mcp_service.app.mcp") as mock_mcp:
+ init_fastmcp_server()
- # Call with custom name to force create_mcp_app path
- init_fastmcp_server(name="Custom Name")
-
- # Verify create_mcp_app was called
- assert mock_create.called
- # Verify instructions use Superset branding (not Apache Superset)
- call_kwargs = mock_create.call_args[1]
- assert "Superset MCP" in call_kwargs["instructions"]
- assert "Superset dashboards" in call_kwargs["instructions"]
+ # Verify the global mcp instance was configured with Superset
branding
+ assert "Superset MCP" in mock_mcp._mcp_server.instructions
+ assert "Superset dashboards" in mock_mcp._mcp_server.instructions
def test_init_fastmcp_server_with_custom_app_name():
@@ -93,20 +86,13 @@ def test_init_fastmcp_server_with_custom_app_name():
"sys.modules",
{"superset.mcp_service.flask_singleton":
MagicMock(app=mock_flask_app)},
):
- with patch("superset.mcp_service.app.create_mcp_app") as mock_create:
- mock_mcp = MagicMock()
- mock_create.return_value = mock_mcp
-
- # Call with custom name to force create_mcp_app path
- init_fastmcp_server(name="Custom Name")
+ with patch("superset.mcp_service.app.mcp") as mock_mcp:
+ init_fastmcp_server()
- # Verify create_mcp_app was called
- assert mock_create.called
# Verify instructions use custom branding
- call_kwargs = mock_create.call_args[1]
- assert custom_app_name in call_kwargs["instructions"]
+ assert custom_app_name in mock_mcp._mcp_server.instructions
# Should not contain default Apache Superset branding
- assert "Apache Superset" not in call_kwargs["instructions"]
+ assert "Apache Superset" not in mock_mcp._mcp_server.instructions
def test_init_fastmcp_server_derives_server_name_from_app_name():
@@ -123,15 +109,44 @@ def
test_init_fastmcp_server_derives_server_name_from_app_name():
"sys.modules",
{"superset.mcp_service.flask_singleton":
MagicMock(app=mock_flask_app)},
):
- with patch("superset.mcp_service.app.create_mcp_app") as mock_create:
- mock_mcp = MagicMock()
- mock_create.return_value = mock_mcp
-
- # Call without name parameter (should use default derived name)
- # Force custom params by passing instructions
- init_fastmcp_server(instructions="custom")
-
- # Verify create_mcp_app was called with derived name
- assert mock_create.called
- call_kwargs = mock_create.call_args[1]
- assert call_kwargs["name"] == expected_server_name
+ with patch("superset.mcp_service.app.mcp") as mock_mcp:
+ init_fastmcp_server()
+
+ # Verify the global mcp instance got the derived name
+ assert mock_mcp._mcp_server.name == expected_server_name
+
+
+def test_init_fastmcp_server_applies_auth_to_global_instance():
+ """Test that auth is applied to the global mcp instance, not a new one."""
+ mock_flask_app = MagicMock()
+ mock_flask_app.config.get.return_value = "Superset"
+ mock_auth = MagicMock()
+
+ with patch.dict(
+ "sys.modules",
+ {"superset.mcp_service.flask_singleton":
MagicMock(app=mock_flask_app)},
+ ):
+ with patch("superset.mcp_service.app.mcp") as mock_mcp:
+ result = init_fastmcp_server(auth=mock_auth)
+
+ # Auth should be set on the global instance
+ assert mock_mcp.auth == mock_auth
+ # Should return the global instance (not a new one)
+ assert result is mock_mcp
+
+
+def test_init_fastmcp_server_applies_middleware_to_global_instance():
+ """Test that middleware is added to the global mcp instance."""
+ mock_flask_app = MagicMock()
+ mock_flask_app.config.get.return_value = "Superset"
+ mock_mw = MagicMock()
+
+ with patch.dict(
+ "sys.modules",
+ {"superset.mcp_service.flask_singleton":
MagicMock(app=mock_flask_app)},
+ ):
+ with patch("superset.mcp_service.app.mcp") as mock_mcp:
+ init_fastmcp_server(middleware=[mock_mw])
+
+ # Middleware should be added via add_middleware
+ mock_mcp.add_middleware.assert_called_once_with(mock_mw)