This is an automated email from the ASF dual-hosted git repository.

aminghadersohi pushed a commit to branch oss-40340
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 9b6ff262fddb926c376d6de140070cd056e8d5b1
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Thu May 28 04:31:36 2026 +0000

    fix(mcp): restore missing tool registrations and fix create_dataset tests
    
    - Restore create_virtual_dataset, query_dataset, get_database_info,
      list_databases, get_chart_sql, get_chart_type_schema, save_sql_query
      imports in app.py (accidentally dropped in the feat commit)
    - Restore create_virtual_dataset and query_dataset exports in dataset
      tool __init__.py
    - Make CreateDatasetRequest.schema optional (str | None, default None)
    - Refactor create_dataset.py to use @tool decorator pattern
    - Fix test_create_dataset.py: convert @patch class decorators to
      with patch() context managers (avoids pytest-asyncio arg injection
      issues), add get_superset_base_url mock for success paths, and
      set certified_by/certification_details=None on mock dataset
---
 superset/mcp_service/app.py                        |   9 +
 superset/mcp_service/dataset/schemas.py            |   8 +-
 superset/mcp_service/dataset/tool/__init__.py      |   8 +-
 .../mcp_service/dataset/tool/create_dataset.py     |  68 +++----
 .../dataset/tool/test_create_dataset.py            | 206 ++++++++++++---------
 5 files changed, 172 insertions(+), 127 deletions(-)

diff --git a/superset/mcp_service/app.py b/superset/mcp_service/app.py
index 0198b6252f3..71d831a2ef9 100644
--- a/superset/mcp_service/app.py
+++ b/superset/mcp_service/app.py
@@ -555,6 +555,8 @@ from superset.mcp_service.chart.tool import (  # noqa: 
F401, E402
     get_chart_data,
     get_chart_info,
     get_chart_preview,
+    get_chart_sql,
+    get_chart_type_schema,
     list_charts,
     update_chart,
     update_chart_preview,
@@ -565,10 +567,16 @@ from superset.mcp_service.dashboard.tool import (  # 
noqa: F401, E402
     get_dashboard_info,
     list_dashboards,
 )
+from superset.mcp_service.database.tool import (  # noqa: F401, E402
+    get_database_info,
+    list_databases,
+)
 from superset.mcp_service.dataset.tool import (  # noqa: F401, E402
     create_dataset,
+    create_virtual_dataset,
     get_dataset_info,
     list_datasets,
+    query_dataset,
 )
 from superset.mcp_service.explore.tool import (  # noqa: F401, E402
     generate_explore_link,
@@ -576,6 +584,7 @@ from superset.mcp_service.explore.tool import (  # noqa: 
F401, E402
 from superset.mcp_service.sql_lab.tool import (  # noqa: F401, E402
     execute_sql,
     open_sql_lab_with_context,
+    save_sql_query,
 )
 from superset.mcp_service.system import (  # noqa: F401, E402
     prompts as system_prompts,
diff --git a/superset/mcp_service/dataset/schemas.py 
b/superset/mcp_service/dataset/schemas.py
index 0bbc4061f8c..cf51d0afd00 100644
--- a/superset/mcp_service/dataset/schemas.py
+++ b/superset/mcp_service/dataset/schemas.py
@@ -334,8 +334,12 @@ class CreateDatasetRequest(BaseModel):
         ),
     ]
     schema: Annotated[
-        str,
-        Field(description="Schema (namespace) where the table lives, e.g. 
'public'"),
+        str | None,
+        Field(
+            default=None,
+            description="Schema (namespace) where the table lives, e.g. 
'public'. "
+            "Optional: omit to use the database default schema.",
+        ),
     ]
     table_name: Annotated[
         str,
diff --git a/superset/mcp_service/dataset/tool/__init__.py 
b/superset/mcp_service/dataset/tool/__init__.py
index 025b4ae1b9a..6fd3c12133c 100644
--- a/superset/mcp_service/dataset/tool/__init__.py
+++ b/superset/mcp_service/dataset/tool/__init__.py
@@ -16,11 +16,15 @@
 # under the License.
 
 from .create_dataset import create_dataset
+from .create_virtual_dataset import create_virtual_dataset
 from .get_dataset_info import get_dataset_info
 from .list_datasets import list_datasets
+from .query_dataset import query_dataset
 
 __all__ = [
-    "list_datasets",
-    "get_dataset_info",
     "create_dataset",
+    "create_virtual_dataset",
+    "get_dataset_info",
+    "list_datasets",
+    "query_dataset",
 ]
diff --git a/superset/mcp_service/dataset/tool/create_dataset.py 
b/superset/mcp_service/dataset/tool/create_dataset.py
index 6484f8ab36d..7c2cc3c0d8c 100644
--- a/superset/mcp_service/dataset/tool/create_dataset.py
+++ b/superset/mcp_service/dataset/tool/create_dataset.py
@@ -25,12 +25,12 @@ the resulting dataset_id directly into generate_chart.
 """
 
 import logging
-from datetime import datetime, timezone
+from typing import Any
 
 from fastmcp import Context
+from superset_core.mcp.decorators import tool, ToolAnnotations
 
-from superset.mcp_service.app import mcp
-from superset.mcp_service.auth import mcp_auth_hook
+from superset.extensions import event_logger
 from superset.mcp_service.dataset.schemas import (
     CreateDatasetRequest,
     DatasetError,
@@ -41,9 +41,17 @@ from superset.mcp_service.dataset.schemas import (
 logger = logging.getLogger(__name__)
 
 
[email protected](tags=["mutate"])
-@mcp_auth_hook
-def create_dataset(
+@tool(
+    tags=["mutate"],
+    class_permission_name="Dataset",
+    method_permission_name="write",
+    annotations=ToolAnnotations(
+        title="Create dataset",
+        readOnlyHint=False,
+        destructiveHint=False,
+    ),
+)
+async def create_dataset(
     request: CreateDatasetRequest, ctx: Context
 ) -> DatasetInfo | DatasetError:
     """Register a physical table as a Superset dataset.
@@ -55,10 +63,10 @@ def create_dataset(
 
     Required fields:
     - database_id: ID of the existing database connection
-    - schema: Schema/namespace where the table lives (e.g. "public")
     - table_name: Exact name of the physical table to register
 
     Optional fields:
+    - schema: Schema/namespace where the table lives (e.g. "public")
     - owners: List of user IDs to set as owners (defaults to calling user)
 
     Example:
@@ -73,6 +81,10 @@ def create_dataset(
     Returns DatasetInfo on success or DatasetError on failure.
     Use list_databases to find the correct database_id.
     """
+    await ctx.info(
+        "Creating dataset: database_id=%s, schema=%r, table_name=%r"
+        % (request.database_id, request.schema, request.table_name)
+    )
     try:
         from superset.commands.dataset.create import CreateDatasetCommand
         from superset.commands.dataset.exceptions import (
@@ -82,7 +94,7 @@ def create_dataset(
             TableNotFoundValidationError,
         )
 
-        dataset_properties = {
+        dataset_properties: dict[str, Any] = {
             "database": request.database_id,
             "schema": request.schema,
             "table_name": request.table_name,
@@ -90,15 +102,15 @@ def create_dataset(
         if request.owners is not None:
             dataset_properties["owners"] = request.owners
 
-        command = CreateDatasetCommand(dataset_properties)
-        dataset = command.run()
+        with event_logger.log_context(action="mcp.create_dataset"):
+            command = CreateDatasetCommand(dataset_properties)
+            dataset = command.run()
 
         result = serialize_dataset_object(dataset)
         if result is None:
-            return DatasetError(
+            return DatasetError.create(
                 error="Dataset was created but could not be serialized",
                 error_type="SerializationError",
-                timestamp=datetime.now(timezone.utc),
             )
 
         logger.info(
@@ -110,33 +122,21 @@ def create_dataset(
         return result
 
     except DatasetExistsValidationError as e:
-        return DatasetError(
-            error=str(e),
-            error_type="DatasetExistsError",
-            timestamp=datetime.now(timezone.utc),
-        )
+        await ctx.error("Dataset already exists: %s" % (str(e),))
+        return DatasetError.create(error=str(e), 
error_type="DatasetExistsError")
     except TableNotFoundValidationError as e:
-        return DatasetError(
-            error=str(e),
-            error_type="TableNotFoundError",
-            timestamp=datetime.now(timezone.utc),
-        )
+        await ctx.error("Table not found: %s" % (str(e),))
+        return DatasetError.create(error=str(e), 
error_type="TableNotFoundError")
     except DatasetInvalidError as e:
-        return DatasetError(
-            error=str(e),
-            error_type="ValidationError",
-            timestamp=datetime.now(timezone.utc),
-        )
+        await ctx.error("Dataset validation failed: %s" % (str(e),))
+        return DatasetError.create(error=str(e), error_type="ValidationError")
     except DatasetCreateFailedError as e:
-        return DatasetError(
-            error=str(e),
-            error_type="CreateFailedError",
-            timestamp=datetime.now(timezone.utc),
-        )
+        await ctx.error("Dataset creation failed: %s" % (str(e),))
+        return DatasetError.create(error=str(e), 
error_type="CreateFailedError")
     except Exception as e:
         logger.error("Failed to create dataset: %s", e, exc_info=True)
-        return DatasetError(
+        await ctx.error("Unexpected error: %s: %s" % (type(e).__name__, 
str(e)))
+        return DatasetError.create(
             error=f"Failed to create dataset: {str(e)}",
             error_type="InternalError",
-            timestamp=datetime.now(timezone.utc),
         )
diff --git a/tests/unit_tests/mcp_service/dataset/tool/test_create_dataset.py 
b/tests/unit_tests/mcp_service/dataset/tool/test_create_dataset.py
index ba2898eb660..01b0a0813c8 100644
--- a/tests/unit_tests/mcp_service/dataset/tool/test_create_dataset.py
+++ b/tests/unit_tests/mcp_service/dataset/tool/test_create_dataset.py
@@ -52,6 +52,8 @@ def _make_mock_dataset(
     dataset.owners = []
     dataset.is_virtual = False
     dataset.database_id = 1
+    dataset.certified_by = None
+    dataset.certification_details = None
     dataset.schema_perm = f"[{database_name}].[{schema}]"
     dataset.url = f"/tablemodelview/edit/{dataset_id}"
     dataset.database = MagicMock()
@@ -87,26 +89,34 @@ def mock_auth():
 class TestCreateDataset:
     """Tests for the create_dataset MCP tool."""
 
-    @patch("superset.commands.dataset.create.CreateDatasetCommand")
     @pytest.mark.asyncio
-    async def test_create_dataset_success(self, mock_command_class, 
mcp_server):
+    async def test_create_dataset_success(self, mcp_server):
         """Happy path: tool creates dataset and returns DatasetInfo."""
         mock_dataset = _make_mock_dataset()
         mock_command = MagicMock()
         mock_command.run.return_value = mock_dataset
-        mock_command_class.return_value = mock_command
 
-        async with Client(mcp_server) as client:
-            result = await client.call_tool(
-                "create_dataset",
-                {
-                    "request": {
-                        "database_id": 1,
-                        "schema": "public",
-                        "table_name": "orders",
-                    }
-                },
-            )
+        with (
+            patch(
+                "superset.commands.dataset.create.CreateDatasetCommand",
+                return_value=mock_command,
+            ) as mock_command_class,
+            patch(
+                "superset.mcp_service.utils.url_utils.get_superset_base_url",
+                return_value="http://localhost:8088";,
+            ),
+        ):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_dataset",
+                    {
+                        "request": {
+                            "database_id": 1,
+                            "schema": "public",
+                            "table_name": "orders",
+                        }
+                    },
+                )
 
         assert result.content is not None
         data = json.loads(result.content[0].text)
@@ -121,27 +131,35 @@ class TestCreateDataset:
         assert call_kwargs["table_name"] == "orders"
         assert "owners" not in call_kwargs
 
-    @patch("superset.commands.dataset.create.CreateDatasetCommand")
     @pytest.mark.asyncio
-    async def test_create_dataset_with_owners(self, mock_command_class, 
mcp_server):
+    async def test_create_dataset_with_owners(self, mcp_server):
         """Owners list is forwarded to the command when supplied."""
         mock_dataset = _make_mock_dataset()
         mock_command = MagicMock()
         mock_command.run.return_value = mock_dataset
-        mock_command_class.return_value = mock_command
 
-        async with Client(mcp_server) as client:
-            result = await client.call_tool(
-                "create_dataset",
-                {
-                    "request": {
-                        "database_id": 2,
-                        "schema": "sales",
-                        "table_name": "transactions",
-                        "owners": [5, 10],
-                    }
-                },
-            )
+        with (
+            patch(
+                "superset.commands.dataset.create.CreateDatasetCommand",
+                return_value=mock_command,
+            ) as mock_command_class,
+            patch(
+                "superset.mcp_service.utils.url_utils.get_superset_base_url",
+                return_value="http://localhost:8088";,
+            ),
+        ):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_dataset",
+                    {
+                        "request": {
+                            "database_id": 2,
+                            "schema": "sales",
+                            "table_name": "transactions",
+                            "owners": [5, 10],
+                        }
+                    },
+                )
 
         data = json.loads(result.content[0].text)
         assert data["id"] == 42
@@ -149,9 +167,8 @@ class TestCreateDataset:
         call_kwargs = mock_command_class.call_args[0][0]
         assert call_kwargs["owners"] == [5, 10]
 
-    @patch("superset.commands.dataset.create.CreateDatasetCommand")
     @pytest.mark.asyncio
-    async def test_create_dataset_already_exists(self, mock_command_class, 
mcp_server):
+    async def test_create_dataset_already_exists(self, mcp_server):
         """Returns DatasetError when a dataset for the table already exists."""
         from superset.commands.dataset.exceptions import 
DatasetExistsValidationError
         from superset.sql.parse import Table
@@ -160,27 +177,29 @@ class TestCreateDataset:
         mock_command.run.side_effect = DatasetExistsValidationError(
             Table("orders", "public", None)
         )
-        mock_command_class.return_value = mock_command
 
-        async with Client(mcp_server) as client:
-            result = await client.call_tool(
-                "create_dataset",
-                {
-                    "request": {
-                        "database_id": 1,
-                        "schema": "public",
-                        "table_name": "orders",
-                    }
-                },
-            )
+        with patch(
+            "superset.commands.dataset.create.CreateDatasetCommand",
+            return_value=mock_command,
+        ):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_dataset",
+                    {
+                        "request": {
+                            "database_id": 1,
+                            "schema": "public",
+                            "table_name": "orders",
+                        }
+                    },
+                )
 
         data = json.loads(result.content[0].text)
         assert data["error_type"] == "DatasetExistsError"
         assert "error" in data
 
-    @patch("superset.commands.dataset.create.CreateDatasetCommand")
     @pytest.mark.asyncio
-    async def test_create_dataset_table_not_found(self, mock_command_class, 
mcp_server):
+    async def test_create_dataset_table_not_found(self, mcp_server):
         """Returns DatasetError when the physical table does not exist in the 
DB."""
         from superset.commands.dataset.exceptions import 
TableNotFoundValidationError
         from superset.sql.parse import Table
@@ -189,44 +208,47 @@ class TestCreateDataset:
         mock_command.run.side_effect = TableNotFoundValidationError(
             Table("missing_table", "public", None)
         )
-        mock_command_class.return_value = mock_command
 
-        async with Client(mcp_server) as client:
-            result = await client.call_tool(
-                "create_dataset",
-                {
-                    "request": {
-                        "database_id": 1,
-                        "schema": "public",
-                        "table_name": "missing_table",
-                    }
-                },
-            )
+        with patch(
+            "superset.commands.dataset.create.CreateDatasetCommand",
+            return_value=mock_command,
+        ):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_dataset",
+                    {
+                        "request": {
+                            "database_id": 1,
+                            "schema": "public",
+                            "table_name": "missing_table",
+                        }
+                    },
+                )
 
         data = json.loads(result.content[0].text)
         assert data["error_type"] == "TableNotFoundError"
 
-    @patch("superset.commands.dataset.create.CreateDatasetCommand")
     @pytest.mark.asyncio
-    async def test_create_dataset_unexpected_error(
-        self, mock_command_class, mcp_server
-    ):
+    async def test_create_dataset_unexpected_error(self, mcp_server):
         """Unexpected exceptions are caught and returned as InternalError."""
         mock_command = MagicMock()
         mock_command.run.side_effect = RuntimeError("DB connection lost")
-        mock_command_class.return_value = mock_command
 
-        async with Client(mcp_server) as client:
-            result = await client.call_tool(
-                "create_dataset",
-                {
-                    "request": {
-                        "database_id": 1,
-                        "schema": "public",
-                        "table_name": "orders",
-                    }
-                },
-            )
+        with patch(
+            "superset.commands.dataset.create.CreateDatasetCommand",
+            return_value=mock_command,
+        ):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_dataset",
+                    {
+                        "request": {
+                            "database_id": 1,
+                            "schema": "public",
+                            "table_name": "orders",
+                        }
+                    },
+                )
 
         data = json.loads(result.content[0].text)
         assert data["error_type"] == "InternalError"
@@ -247,11 +269,8 @@ class TestCreateDataset:
                     },
                 )
 
-    @patch("superset.commands.dataset.create.CreateDatasetCommand")
     @pytest.mark.asyncio
-    async def test_create_dataset_returns_full_dataset_info(
-        self, mock_command_class, mcp_server
-    ):
+    async def test_create_dataset_returns_full_dataset_info(self, mcp_server):
         """The returned DatasetInfo includes columns, metrics, and all core 
fields."""
         mock_dataset = _make_mock_dataset(
             dataset_id=99, table_name="sales", schema="dw"
@@ -277,19 +296,28 @@ class TestCreateDataset:
 
         mock_command = MagicMock()
         mock_command.run.return_value = mock_dataset
-        mock_command_class.return_value = mock_command
 
-        async with Client(mcp_server) as client:
-            result = await client.call_tool(
-                "create_dataset",
-                {
-                    "request": {
-                        "database_id": 1,
-                        "schema": "dw",
-                        "table_name": "sales",
-                    }
-                },
-            )
+        with (
+            patch(
+                "superset.commands.dataset.create.CreateDatasetCommand",
+                return_value=mock_command,
+            ),
+            patch(
+                "superset.mcp_service.utils.url_utils.get_superset_base_url",
+                return_value="http://localhost:8088";,
+            ),
+        ):
+            async with Client(mcp_server) as client:
+                result = await client.call_tool(
+                    "create_dataset",
+                    {
+                        "request": {
+                            "database_id": 1,
+                            "schema": "dw",
+                            "table_name": "sales",
+                        }
+                    },
+                )
 
         data = json.loads(result.content[0].text)
         assert data["id"] == 99

Reply via email to