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