fix(mcp): clarify request wrapper in list_datasets, list_charts, list_dashboards (#39920)

This commit is contained in:
Amin Ghadersohi
2026-05-08 16:01:07 -04:00
committed by GitHub
parent ff7dc53853
commit cfb0b6e811
9 changed files with 158 additions and 14 deletions

View File

@@ -1349,7 +1349,8 @@ class TestDashboardSortableColumns:
# Check list_dashboards docstring for sortable columns documentation
assert list_dashboards.__doc__ is not None
assert "Sortable columns for order_column:" in list_dashboards.__doc__
assert "Sortable columns for" in list_dashboards.__doc__
assert "order_column" in list_dashboards.__doc__
for col in SORTABLE_DASHBOARD_COLUMNS:
assert col in list_dashboards.__doc__

View File

@@ -1700,7 +1700,8 @@ class TestDatasetSortableColumns:
# Check list_datasets docstring for sortable columns documentation
assert list_datasets.__doc__ is not None
assert "Sortable columns for order_column:" in list_datasets.__doc__
assert "Sortable columns for" in list_datasets.__doc__
assert "order_column" in list_datasets.__doc__
for col in SORTABLE_DATASET_COLUMNS:
assert col in list_datasets.__doc__
@@ -2080,3 +2081,90 @@ class TestListDatasetsOwnedByMe:
request = ListDatasetsRequest(owned_by_me=True, created_by_me=True)
assert request.owned_by_me is True
assert request.created_by_me is True
class TestListDatasetsRequestWrapper:
"""
Tests verifying that list_datasets requires a ``request`` wrapper object.
LLMs sometimes pass parameters like ``search``, ``page``, or ``page_size``
as flat top-level kwargs instead of nesting them inside a ``request``
object. These tests confirm the correct call shape through both the Pydantic
schema and the actual MCP tool layer, and verify that invalid filter column
names (e.g. ``created_by_fk``) are rejected.
"""
def test_request_wrapper_with_search(self) -> None:
"""Parameters passed inside request= are accepted by the schema."""
request = ListDatasetsRequest(search="sales", page=1, page_size=10)
assert request.search == "sales"
assert request.page == 1
assert request.page_size == 10
def test_request_wrapper_defaults(self) -> None:
"""No-arg constructor produces valid schema defaults."""
request = ListDatasetsRequest()
assert request.search is None
assert request.page == 1
assert request.filters == []
def test_dataset_filter_valid_col(self) -> None:
"""Valid col values are accepted by DatasetFilter."""
for col in ("table_name", "schema", "database_name"):
f = DatasetFilter(col=col, opr="sw", value="test")
assert f.col == col
def test_dataset_filter_invalid_col_raises(self) -> None:
"""Column names not in the Literal are rejected with a validation error.
This guards against LLMs passing ``created_by_fk`` or similar
internal column names that are not exposed as filter fields.
"""
from pydantic import ValidationError
for bad_col in ("created_by_fk", "id", "database_id", "owner"):
with pytest.raises(ValidationError):
DatasetFilter(col=bad_col, opr="eq", value="1")
@patch("superset.daos.dataset.DatasetDAO.list")
@pytest.mark.asyncio
async def test_request_wrapper_enforced_by_tool(
self, mock_list, mcp_server
) -> None:
"""The MCP tool layer accepts the request wrapper and returns results.
Verifies end-to-end that wrapping params in ``request={}`` works through
the actual FastMCP tool call, not just schema validation.
"""
mock_list.return_value = ([], 0)
async with Client(mcp_server) as client:
result = await client.call_tool(
"list_datasets",
{"request": {"search": "sales", "page": 1, "page_size": 5}},
)
data = json.loads(result.content[0].text)
assert data["count"] == 0
assert data["datasets"] == []
@pytest.mark.asyncio
async def test_flat_kwargs_rejected(self, mcp_server) -> None:
"""Passing search/page/page_size as top-level kwargs raises a ToolError
that specifically mentions the unexpected arguments.
This is the exact failure pattern from story #105712: LLMs call
``list_datasets(search=..., page=..., page_size=...)`` instead of
``list_datasets(request={...})``.
"""
with pytest.raises(ToolError) as exc_info:
async with Client(mcp_server) as client:
await client.call_tool(
"list_datasets",
{"search": "sales", "page": 1, "page_size": 10},
)
error_text = str(exc_info.value)
# The error must call out the unexpected arguments, not some unrelated failure
assert (
"search" in error_text
or "Unexpected" in error_text
or "request" in error_text
)