mirror of
https://github.com/apache/superset.git
synced 2026-04-07 18:35:15 +00:00
feat(mcp): dynamic feature availability via menus and feature flags (#37964)
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
60
tests/unit_tests/mcp_service/system/test_system_utils.py
Normal file
60
tests/unit_tests/mcp_service/system/test_system_utils.py
Normal file
@@ -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"]
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user