fix(mcp): fix crashes in list tools, dataset info, chart preview, and add owner/favorite filters (#38277)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Amin Ghadersohi
2026-03-13 12:46:52 +01:00
committed by GitHub
parent f458e2d484
commit d5cf77cd60
25 changed files with 297 additions and 72 deletions

View File

@@ -64,6 +64,21 @@ def _mock_chart(id: int = 1, slice_name: str = "Test Chart") -> Mock:
chart.id = id
chart.slice_name = slice_name
chart.uuid = f"chart-uuid-{id}"
chart.tags = []
chart.owners = []
chart.viz_type = "table"
chart.datasource_name = None
chart.datasource_type = None
chart.description = None
chart.cache_timeout = None
chart.changed_by = None
chart.changed_by_name = None
chart.changed_on = None
chart.changed_on_humanized = None
chart.created_by = None
chart.created_by_name = None
chart.created_on = None
chart.created_on_humanized = None
return chart
@@ -390,7 +405,7 @@ class TestAddChartToExistingDashboard:
):
"""Test adding a chart to an existing dashboard."""
mock_dashboard = _mock_dashboard(id=1, title="Existing Dashboard")
mock_dashboard.slices = [Mock(id=10), Mock(id=20)]
mock_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=20)]
mock_dashboard.position_json = json.dumps(
{
"ROOT_ID": {
@@ -430,7 +445,11 @@ class TestAddChartToExistingDashboard:
mock_db_session.get.return_value = mock_chart
updated_dashboard = _mock_dashboard(id=1, title="Existing Dashboard")
updated_dashboard.slices = [Mock(id=10), Mock(id=20), Mock(id=30)]
updated_dashboard.slices = [
_mock_chart(id=10),
_mock_chart(id=20),
_mock_chart(id=30),
]
mock_update_command.return_value.run.return_value = updated_dashboard
request = {"dashboard_id": 1, "chart_id": 30}
@@ -505,7 +524,7 @@ class TestAddChartToExistingDashboard:
):
"""Test error when chart is already in dashboard."""
mock_dashboard = _mock_dashboard()
mock_dashboard.slices = [Mock(id=5)]
mock_dashboard.slices = [_mock_chart(id=5)]
mock_find_dashboard.return_value = mock_dashboard
mock_db_session.get.return_value = _mock_chart(id=5)
request = {"dashboard_id": 1, "chart_id": 5}
@@ -537,7 +556,7 @@ class TestAddChartToExistingDashboard:
mock_db_session.get.return_value = mock_chart
updated_dashboard = _mock_dashboard(id=2)
updated_dashboard.slices = [Mock(id=15)]
updated_dashboard.slices = [_mock_chart(id=15)]
mock_update_command.return_value.run.return_value = updated_dashboard
request = {"dashboard_id": 2, "chart_id": 15}
@@ -585,7 +604,7 @@ class TestAddChartToExistingDashboard:
):
"""Test adding chart to a dashboard that uses tabs."""
mock_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
mock_dashboard.slices = [Mock(id=10)]
mock_dashboard.slices = [_mock_chart(id=10)]
mock_dashboard.position_json = json.dumps(
{
"ROOT_ID": {
@@ -646,7 +665,7 @@ class TestAddChartToExistingDashboard:
mock_db_session.get.return_value = mock_chart
updated_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
updated_dashboard.slices = [Mock(id=10), Mock(id=25)]
updated_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=25)]
mock_update_command.return_value.run.return_value = updated_dashboard
request = {"dashboard_id": 3, "chart_id": 25}
@@ -685,7 +704,7 @@ class TestAddChartToExistingDashboard:
):
"""Test adding chart to a specific tab using target_tab name."""
mock_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
mock_dashboard.slices = [Mock(id=10)]
mock_dashboard.slices = [_mock_chart(id=10)]
mock_dashboard.position_json = json.dumps(
{
"ROOT_ID": {
@@ -746,7 +765,7 @@ class TestAddChartToExistingDashboard:
mock_db_session.get.return_value = mock_chart
updated_dashboard = _mock_dashboard(id=3, title="Tabbed Dashboard")
updated_dashboard.slices = [Mock(id=10), Mock(id=30)]
updated_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=30)]
mock_update_command.return_value.run.return_value = updated_dashboard
request = {"dashboard_id": 3, "chart_id": 30, "target_tab": "Customers"}
@@ -786,7 +805,7 @@ class TestAddChartToExistingDashboard:
):
"""Test adding chart to dashboard that has nanoid-style ROW IDs."""
mock_dashboard = _mock_dashboard(id=4, title="Nanoid Dashboard")
mock_dashboard.slices = [Mock(id=10)]
mock_dashboard.slices = [_mock_chart(id=10)]
mock_dashboard.position_json = json.dumps(
{
"ROOT_ID": {
@@ -821,7 +840,7 @@ class TestAddChartToExistingDashboard:
mock_db_session.get.return_value = mock_chart
updated_dashboard = _mock_dashboard(id=4, title="Nanoid Dashboard")
updated_dashboard.slices = [Mock(id=10), Mock(id=50)]
updated_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=50)]
mock_update_command.return_value.run.return_value = updated_dashboard
request = {"dashboard_id": 4, "chart_id": 50}

View File

@@ -1319,7 +1319,7 @@ class TestDatasetDefaultColumnFiltering:
dataset_item = data["datasets"][0]
# Verify ONLY default columns are present in the response item
expected_keys = {"id", "table_name", "schema_name", "changed_on_humanized"}
expected_keys = {"id", "table_name", "schema", "changed_on_humanized"}
actual_keys = set(dataset_item.keys())
# The response should only contain the default columns, NOT all columns

View File

@@ -702,6 +702,41 @@ class TestExecuteSql:
with pytest.raises(ToolError, match="less than or equal to 10000"):
await client.call_tool("execute_sql", {"request": request})
@patch("superset.security_manager")
@patch("superset.db")
@pytest.mark.asyncio
async def test_execute_sql_no_limit_respects_sql(
self, mock_db, mock_security_manager, mcp_server
):
"""Test that omitting limit lets the SQL LIMIT clause be respected."""
mock_database = _mock_database()
mock_database.execute.return_value = _create_select_result(
rows=[{"id": i} for i in range(5)],
columns=["id"],
original_sql="SELECT id FROM users LIMIT 5",
)
mock_db.session.query.return_value.filter_by.return_value.first.return_value = (
mock_database
)
mock_security_manager.can_access_database.return_value = True
# No 'limit' key — should default to None (no override)
request = {
"database_id": 1,
"sql": "SELECT id FROM users LIMIT 5",
}
async with Client(mcp_server) as client:
result = await client.call_tool("execute_sql", {"request": request})
data = result.structured_content
assert data["success"] is True
# Verify limit=None was passed to QueryOptions (no override)
call_args = mock_database.execute.call_args
options = call_args[0][1]
assert options.limit is None
@patch("superset.security_manager")
@patch("superset.db")
@pytest.mark.asyncio

View File

@@ -366,6 +366,6 @@ def test_chart_filter_existing_columns_still_work():
def test_dashboard_filter_existing_columns_still_work():
"""Test that pre-existing dashboard filter columns are not broken."""
for col in ("dashboard_title", "published", "favorite"):
for col in ("dashboard_title", "published", "created_by_fk"):
f = DashboardFilter(col=col, opr="eq", value="test")
assert f.col == col

View File

@@ -95,10 +95,11 @@ def test_model_list_tool_basic():
assert result.count == 2
assert result.total_count == 2
assert isinstance(result.items[0], DummyOutputSchema)
assert result.page == 1
# run_tool receives 0-based page; response reports 1-based (page+1)
assert result.page == 2
assert result.page_size == 2
assert result.total_pages == 1
# For page=1, ModelListCore sets has_previous=True
# For page=1 (0-based), ModelListCore sets has_previous=True
assert result.has_previous is True
assert result.has_next is False
assert result.columns_requested == ["id", "name"]