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 1cd35bb1029 feat(mcp): dynamic feature availability via menus and 
feature flags (#37964)
1cd35bb1029 is described below

commit 1cd35bb1029287c73f60dcd1a007eb078def848a
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Wed Feb 25 06:01:44 2026 -0500

    feat(mcp): dynamic feature availability via menus and feature flags (#37964)
---
 superset/mcp_service/app.py                        |  4 ++
 superset/mcp_service/system/schemas.py             | 22 ++++++++
 superset/mcp_service/system/system_utils.py        | 29 +++++++++++
 .../mcp_service/system/tool/get_instance_info.py   |  2 +
 .../mcp_service/system/test_system_utils.py        | 60 ++++++++++++++++++++++
 .../system/tool/test_get_current_user.py           |  2 +
 tests/unit_tests/mcp_service/test_mcp_config.py    |  9 ++++
 7 files changed, 128 insertions(+)

diff --git a/superset/mcp_service/app.py b/superset/mcp_service/app.py
index f43df61e487..5db973067e1 100644
--- a/superset/mcp_service/app.py
+++ b/superset/mcp_service/app.py
@@ -152,6 +152,10 @@ Input format:
 - When MCP_PARSE_REQUEST_ENABLED is True (default), string-serialized JSON is 
also
   accepted as input, which works around double-serialization bugs in some MCP 
clients
 
+Feature Availability:
+- Call get_instance_info to discover accessible menus for the current user.
+- Do NOT assume features exist; always check get_instance_info first.
+
 If you are unsure which tool to use, start with get_instance_info
 or use the quickstart prompt for an interactive guide.
 
diff --git a/superset/mcp_service/system/schemas.py 
b/superset/mcp_service/system/schemas.py
index 93b676a0e1a..f59243f4e65 100644
--- a/superset/mcp_service/system/schemas.py
+++ b/superset/mcp_service/system/schemas.py
@@ -108,6 +108,22 @@ class PopularContent(BaseModel):
     top_creators: List[str] = Field(..., description="Most active creators")
 
 
+class FeatureAvailability(BaseModel):
+    """Dynamic feature availability for the current user and deployment.
+
+    Menus are detected at request time from the security manager,
+    so they reflect the actual permissions of the requesting user.
+    """
+
+    accessible_menus: List[str] = Field(
+        default_factory=list,
+        description=(
+            "UI menu items accessible to the current user, "
+            "derived from FAB role permissions"
+        ),
+    )
+
+
 class InstanceInfo(BaseModel):
     instance_summary: InstanceSummary = Field(
         ..., description="Instance summary information"
@@ -129,6 +145,12 @@ class InstanceInfo(BaseModel):
         description="The authenticated user making the request. "
         "Use current_user.id with created_by_fk filter to find your own 
assets.",
     )
+    feature_availability: FeatureAvailability = Field(
+        ...,
+        description=(
+            "Dynamic feature availability for the current user and deployment"
+        ),
+    )
     timestamp: datetime = Field(..., description="Response timestamp")
 
 
diff --git a/superset/mcp_service/system/system_utils.py 
b/superset/mcp_service/system/system_utils.py
index 43426df4716..b9a4b8759f3 100644
--- a/superset/mcp_service/system/system_utils.py
+++ b/superset/mcp_service/system/system_utils.py
@@ -22,16 +22,20 @@ This module contains helper functions used by system tools 
for calculating
 instance metrics, dashboard breakdowns, database breakdowns, and activity 
summaries.
 """
 
+import logging
 from typing import Any, Dict
 
 from superset.mcp_service.system.schemas import (
     DashboardBreakdown,
     DatabaseBreakdown,
+    FeatureAvailability,
     InstanceSummary,
     PopularContent,
     RecentActivity,
 )
 
+logger = logging.getLogger(__name__)
+
 
 def calculate_dashboard_breakdown(
     base_counts: Dict[str, int],
@@ -194,3 +198,28 @@ def calculate_popular_content(
         top_tags=[],
         top_creators=[],
     )
+
+
+def calculate_feature_availability(
+    base_counts: Dict[str, int],
+    time_metrics: Dict[str, Dict[str, int]],
+    dao_classes: Dict[str, Any],
+) -> FeatureAvailability:
+    """Detect available features dynamically from menus.
+
+    Queries the FAB security manager for menu items accessible to the
+    current user.
+    """
+    accessible_menus: list[str] = []
+
+    try:
+        from superset import security_manager
+
+        menu_names = security_manager.user_view_menu_names("menu_access")
+        accessible_menus = sorted(menu_names)
+    except Exception as exc:
+        logger.debug("Could not retrieve accessible menus: %s", exc)
+
+    return FeatureAvailability(
+        accessible_menus=accessible_menus,
+    )
diff --git a/superset/mcp_service/system/tool/get_instance_info.py 
b/superset/mcp_service/system/tool/get_instance_info.py
index 8d383ae167f..5aadd36ecbd 100644
--- a/superset/mcp_service/system/tool/get_instance_info.py
+++ b/superset/mcp_service/system/tool/get_instance_info.py
@@ -35,6 +35,7 @@ from superset.mcp_service.system.schemas import (
 from superset.mcp_service.system.system_utils import (
     calculate_dashboard_breakdown,
     calculate_database_breakdown,
+    calculate_feature_availability,
     calculate_instance_summary,
     calculate_popular_content,
     calculate_recent_activity,
@@ -61,6 +62,7 @@ _instance_info_core = InstanceInfoCore(
         "dashboard_breakdown": calculate_dashboard_breakdown,
         "database_breakdown": calculate_database_breakdown,
         "popular_content": calculate_popular_content,
+        "feature_availability": calculate_feature_availability,
     },
     time_windows={
         "recent": 7,
diff --git a/tests/unit_tests/mcp_service/system/test_system_utils.py 
b/tests/unit_tests/mcp_service/system/test_system_utils.py
new file mode 100644
index 00000000000..8b3536dd689
--- /dev/null
+++ b/tests/unit_tests/mcp_service/system/test_system_utils.py
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for system-level utility functions."""
+
+from unittest.mock import MagicMock, patch
+
+from superset.mcp_service.system.system_utils import 
calculate_feature_availability
+
+
+def test_calculate_feature_availability_returns_menus():
+    """Test that accessible menus are returned."""
+    mock_sm = MagicMock()
+    mock_sm.user_view_menu_names.return_value = {
+        "SQL Lab",
+        "Dashboards",
+        "Charts",
+    }
+
+    with patch("superset.security_manager", mock_sm):
+        result = calculate_feature_availability({}, {}, {})
+
+    assert result.accessible_menus == ["Charts", "Dashboards", "SQL Lab"]
+    mock_sm.user_view_menu_names.assert_called_once_with("menu_access")
+
+
+def test_calculate_feature_availability_empty_when_no_context():
+    """Test graceful fallback when security manager is unavailable."""
+    broken_sm = MagicMock()
+    broken_sm.user_view_menu_names.side_effect = RuntimeError("no ctx")
+
+    with patch("superset.security_manager", broken_sm):
+        result = calculate_feature_availability({}, {}, {})
+
+    assert result.accessible_menus == []
+
+
+def test_calculate_feature_availability_menus_sorted():
+    """Test that accessible menus are returned in sorted order."""
+    mock_sm = MagicMock()
+    mock_sm.user_view_menu_names.return_value = {"Zzz", "Aaa", "Mmm"}
+
+    with patch("superset.security_manager", mock_sm):
+        result = calculate_feature_availability({}, {}, {})
+
+    assert result.accessible_menus == ["Aaa", "Mmm", "Zzz"]
diff --git a/tests/unit_tests/mcp_service/system/tool/test_get_current_user.py 
b/tests/unit_tests/mcp_service/system/tool/test_get_current_user.py
index 91d8fbc6e8b..74000ae8945 100644
--- a/tests/unit_tests/mcp_service/system/tool/test_get_current_user.py
+++ b/tests/unit_tests/mcp_service/system/tool/test_get_current_user.py
@@ -60,6 +60,7 @@ def _make_instance_info(**kwargs):
     from superset.mcp_service.system.schemas import (
         DashboardBreakdown,
         DatabaseBreakdown,
+        FeatureAvailability,
         InstanceSummary,
         PopularContent,
         RecentActivity,
@@ -93,6 +94,7 @@ def _make_instance_info(**kwargs):
         ),
         "database_breakdown": DatabaseBreakdown(by_type={}),
         "popular_content": PopularContent(top_tags=[], top_creators=[]),
+        "feature_availability": FeatureAvailability(),
         "timestamp": datetime.now(timezone.utc),
     }
     defaults.update(kwargs)
diff --git a/tests/unit_tests/mcp_service/test_mcp_config.py 
b/tests/unit_tests/mcp_service/test_mcp_config.py
index 6b466706d07..73f8cf45cbf 100644
--- a/tests/unit_tests/mcp_service/test_mcp_config.py
+++ b/tests/unit_tests/mcp_service/test_mcp_config.py
@@ -55,6 +55,15 @@ def test_get_default_instructions_with_enterprise_branding():
     assert "execute_sql" in instructions
 
 
+def test_get_default_instructions_mentions_feature_availability():
+    """Test that instructions direct LLMs to get_instance_info for features."""
+    instructions = get_default_instructions()
+
+    assert "get_instance_info" in instructions
+    assert "Feature Availability" in instructions
+    assert "accessible menus" in instructions
+
+
 def test_init_fastmcp_server_with_default_app_name():
     """Test that default APP_NAME produces Superset branding."""
     # Mock Flask app config with default APP_NAME

Reply via email to