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)

Reply via email to