feat(mcp): dynamic feature availability via menus and feature flags (#37964)

This commit is contained in:
Amin Ghadersohi
2026-02-25 06:01:44 -05:00
committed by GitHub
parent 5eb35a4795
commit 1cd35bb102
7 changed files with 128 additions and 0 deletions

View File

@@ -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.

View File

@@ -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")

View File

@@ -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,
)

View File

@@ -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,

View 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"]

View File

@@ -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)

View File

@@ -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