You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by mi...@apache.org on 2026/03/10 11:26:44 UTC
(superset) branch 6.1 created (now a9b24da0a2e)
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a change to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
at a9b24da0a2e fix(mcp): improve default chart names with descriptive format (#38406)
This branch includes the following new commits:
new 6ebaf5919f2 fix(docs): swizzle MethodEndpoint to fix SSG crash on all API pages (#38533)
new 77c7f9b5e8f fix(mcp): make fastmcp truly optional during Superset startup (#38534)
new 8d7a36df5ae fix(mcp): resolve chatbot tool call flakiness with URL and instruction fixes (#38532)
new d8e346d52da fix(embedded): add CurrentUserRestApi read permission to Public role defaults (#38474)
new d42caf744fa feat(mcp): register GlobalErrorHandlerMiddleware and LoggingMiddleware (#38523)
new 10415fe8be7 fix(mcp): add missing command.validate() to MCP chart data tools (#38521)
new d266146bbb9 fix(mcp): add guardrails to prevent LLM artifact generation (#38391)
new ab64ad7ac71 fix(mcp): add missing __init__.py for chart, dashboard, dataset packages (#38400)
new a9b24da0a2e fix(mcp): improve default chart names with descriptive format (#38406)
The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(superset) 08/09: fix(mcp): add missing __init__.py for chart, dashboard, dataset packages (#38400)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit ab64ad7ac719c214ee8e20eda23170ae5e540ef1
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Tue Mar 10 09:52:48 2026 +0100
fix(mcp): add missing __init__.py for chart, dashboard, dataset packages (#38400)
(cherry picked from commit 5c2cbb58bc98f6af4f3ca274b126a76e224d20d9)
---
.../mcp_service/chart/__init__.py | 23 ----------
.../mcp_service/dashboard/__init__.py | 23 ----------
.../mcp_service/dataset/__init__.py | 23 ----------
.../mcp_service/test_mcp_tool_registration.py | 53 ++++++++++++++++++++++
4 files changed, 53 insertions(+), 69 deletions(-)
diff --git a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py b/superset/mcp_service/chart/__init__.py
similarity index 56%
copy from tests/unit_tests/mcp_service/test_mcp_tool_registration.py
copy to superset/mcp_service/chart/__init__.py
index 088b3ad09a2..13a83393a91 100644
--- a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
+++ b/superset/mcp_service/chart/__init__.py
@@ -14,26 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-"""Test MCP app imports and tool/prompt registration."""
-
-
-def test_mcp_app_imports_successfully():
- """Test that the MCP app can be imported without errors."""
- from superset.mcp_service.app import mcp
-
- assert mcp is not None
- assert hasattr(mcp, "_tool_manager")
-
- tools = mcp._tool_manager._tools
- assert len(tools) > 0
- assert "health_check" in tools
- assert "list_charts" in tools
-
-
-def test_mcp_prompts_registered():
- """Test that MCP prompts are registered."""
- from superset.mcp_service.app import mcp
-
- prompts = mcp._prompt_manager._prompts
- assert len(prompts) > 0
diff --git a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py b/superset/mcp_service/dashboard/__init__.py
similarity index 56%
copy from tests/unit_tests/mcp_service/test_mcp_tool_registration.py
copy to superset/mcp_service/dashboard/__init__.py
index 088b3ad09a2..13a83393a91 100644
--- a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
+++ b/superset/mcp_service/dashboard/__init__.py
@@ -14,26 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-"""Test MCP app imports and tool/prompt registration."""
-
-
-def test_mcp_app_imports_successfully():
- """Test that the MCP app can be imported without errors."""
- from superset.mcp_service.app import mcp
-
- assert mcp is not None
- assert hasattr(mcp, "_tool_manager")
-
- tools = mcp._tool_manager._tools
- assert len(tools) > 0
- assert "health_check" in tools
- assert "list_charts" in tools
-
-
-def test_mcp_prompts_registered():
- """Test that MCP prompts are registered."""
- from superset.mcp_service.app import mcp
-
- prompts = mcp._prompt_manager._prompts
- assert len(prompts) > 0
diff --git a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py b/superset/mcp_service/dataset/__init__.py
similarity index 56%
copy from tests/unit_tests/mcp_service/test_mcp_tool_registration.py
copy to superset/mcp_service/dataset/__init__.py
index 088b3ad09a2..13a83393a91 100644
--- a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
+++ b/superset/mcp_service/dataset/__init__.py
@@ -14,26 +14,3 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
-
-"""Test MCP app imports and tool/prompt registration."""
-
-
-def test_mcp_app_imports_successfully():
- """Test that the MCP app can be imported without errors."""
- from superset.mcp_service.app import mcp
-
- assert mcp is not None
- assert hasattr(mcp, "_tool_manager")
-
- tools = mcp._tool_manager._tools
- assert len(tools) > 0
- assert "health_check" in tools
- assert "list_charts" in tools
-
-
-def test_mcp_prompts_registered():
- """Test that MCP prompts are registered."""
- from superset.mcp_service.app import mcp
-
- prompts = mcp._prompt_manager._prompts
- assert len(prompts) > 0
diff --git a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py b/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
index 088b3ad09a2..be470bc4fd0 100644
--- a/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
+++ b/tests/unit_tests/mcp_service/test_mcp_tool_registration.py
@@ -37,3 +37,56 @@ def test_mcp_prompts_registered():
prompts = mcp._prompt_manager._prompts
assert len(prompts) > 0
+
+
+def test_mcp_resources_registered():
+ """Test that MCP resources are registered.
+
+ Resources are registered via @mcp.resource() decorators in resource files.
+ They require __init__.py in parent packages for find_packages() to include
+ them in distributions. This test ensures all expected resources are found.
+ """
+ from superset.mcp_service.app import mcp
+
+ resource_manager = mcp._resource_manager
+ resources = resource_manager._resources
+ assert len(resources) > 0, "No MCP resources registered"
+
+ # Verify the two documented resources are registered
+ resource_uris = set(resources.keys())
+ assert "chart://configs" in resource_uris, (
+ "chart://configs resource not registered - "
+ "check superset/mcp_service/chart/__init__.py exists"
+ )
+ assert "instance://metadata" in resource_uris, (
+ "instance://metadata resource not registered - "
+ "check superset/mcp_service/system/resources/ imports"
+ )
+
+
+def test_mcp_packages_discoverable_by_setuptools():
+ """Test that all MCP sub-packages have __init__.py for setuptools.
+
+ setuptools.find_packages() only discovers directories with __init__.py.
+ Without __init__.py, sub-packages (tool, resources, prompts) are excluded
+ from built distributions, causing missing module errors in deployments.
+ """
+ from pathlib import Path
+
+ mcp_root = Path(__file__).parents[3] / "superset" / "mcp_service"
+ assert mcp_root.is_dir(), f"MCP service root not found: {mcp_root}"
+
+ # All immediate sub-directories that contain Python files should be packages
+ missing = []
+ for subdir in sorted(mcp_root.iterdir()):
+ if not subdir.is_dir() or subdir.name.startswith(("_", ".")):
+ continue
+ # Check if it has any .py files in it or its subdirectories
+ has_py = any(subdir.rglob("*.py"))
+ if has_py and not (subdir / "__init__.py").exists():
+ missing.append(subdir.name)
+
+ assert not missing, (
+ f"MCP sub-packages missing __init__.py (will be excluded from "
+ f"setuptools distributions): {missing}"
+ )
(superset) 02/09: fix(mcp): make fastmcp truly optional during Superset startup (#38534)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 77c7f9b5e8fad9aac16647976d4d02f92330b42e
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Mon Mar 9 23:32:27 2026 +0100
fix(mcp): make fastmcp truly optional during Superset startup (#38534)
(cherry picked from commit 4cd3ce164de6fff538028b8449c9649687d1c82c)
---
superset/core/mcp/core_mcp_injection.py | 25 +++++++++++++++----------
1 file changed, 15 insertions(+), 10 deletions(-)
diff --git a/superset/core/mcp/core_mcp_injection.py b/superset/core/mcp/core_mcp_injection.py
index f37ae6e48f1..ad4a7b1211d 100644
--- a/superset/core/mcp/core_mcp_injection.py
+++ b/superset/core/mcp/core_mcp_injection.py
@@ -251,23 +251,28 @@ def initialize_core_mcp_dependencies() -> None:
Also imports MCP service app to register all host tools BEFORE extension loading.
"""
- try:
- # Replace the abstract decorators with concrete implementations
+ import superset_core.mcp.decorators
- import superset_core.mcp.decorators
+ try:
+ from fastmcp.tools import Tool # noqa: F401
+ except ImportError:
+ logger.info(
+ "fastmcp is not installed, skipping MCP initialization. "
+ "Install it with: pip install 'apache-superset[fastmcp]'"
+ )
+ return
- superset_core.mcp.decorators.tool = create_tool_decorator
- superset_core.mcp.decorators.prompt = create_prompt_decorator
+ # Replace the abstract decorators with concrete implementations
+ superset_core.mcp.decorators.tool = create_tool_decorator
+ superset_core.mcp.decorators.prompt = create_prompt_decorator
- logger.info("MCP dependency injection initialized successfully")
+ logger.info("MCP dependency injection initialized successfully")
+ try:
# Import MCP service app to register host tools BEFORE extension loading
# This prevents host tools from being registered during extension context
-
from superset.mcp_service import app # noqa: F401
logger.info("MCP service app imported - host tools registered")
-
except Exception as e:
- logger.error("Failed to initialize MCP dependencies: %s", e)
- raise
+ logger.error("Failed to register MCP host tools: %s", e)
(superset) 04/09: fix(embedded): add CurrentUserRestApi read permission to Public role defaults (#38474)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit d8e346d52dac75f3f67944aaa2171d5571641263
Author: Enzo Martellucci <52...@users.noreply.github.com>
AuthorDate: Tue Mar 10 00:08:37 2026 +0100
fix(embedded): add CurrentUserRestApi read permission to Public role defaults (#38474)
(cherry picked from commit a17f38a4e237179c2ea2e8d9994b94134530301b)
---
superset/security/manager.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 9f97c810d77..6af83a4537a 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -424,6 +424,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
("can_read", "Theme"),
# Embedded dashboard support
("can_read", "EmbeddedDashboard"),
+ ("can_read", "CurrentUserRestApi"),
# Datasource metadata for chart rendering
("can_get", "Datasource"),
("can_external_metadata", "Datasource"),
(superset) 06/09: fix(mcp): add missing command.validate() to MCP chart data tools (#38521)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 10415fe8be72fd7b061abe24172478f56223fc4f
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Tue Mar 10 09:50:41 2026 +0100
fix(mcp): add missing command.validate() to MCP chart data tools (#38521)
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
(cherry picked from commit 2a876e8b8686659bd06b1cc0d3d68fbab45cf7f0)
---
superset/mcp_service/chart/preview_utils.py | 1 +
superset/mcp_service/chart/tool/generate_chart.py | 1 +
superset/mcp_service/chart/tool/get_chart_data.py | 1 +
.../mcp_service/chart/tool/get_chart_preview.py | 3 +
.../mcp_service/chart/tool/test_get_chart_data.py | 187 +++++++++++++++++++++
5 files changed, 193 insertions(+)
diff --git a/superset/mcp_service/chart/preview_utils.py b/superset/mcp_service/chart/preview_utils.py
index 677d3034fd4..c28681a9da0 100644
--- a/superset/mcp_service/chart/preview_utils.py
+++ b/superset/mcp_service/chart/preview_utils.py
@@ -103,6 +103,7 @@ def generate_preview_from_form_data(
# Execute query
command = ChartDataCommand(query_context_obj)
+ command.validate()
result = command.run()
if not result or not result.get("queries"):
diff --git a/superset/mcp_service/chart/tool/generate_chart.py b/superset/mcp_service/chart/tool/generate_chart.py
index 6ff3bee59cc..b5711d5e5f7 100644
--- a/superset/mcp_service/chart/tool/generate_chart.py
+++ b/superset/mcp_service/chart/tool/generate_chart.py
@@ -101,6 +101,7 @@ def _compile_chart(
)
command = ChartDataCommand(query_context)
+ command.validate()
result = command.run()
warnings: List[str] = []
diff --git a/superset/mcp_service/chart/tool/get_chart_data.py b/superset/mcp_service/chart/tool/get_chart_data.py
index eaa50ca6b45..9239a278d9e 100644
--- a/superset/mcp_service/chart/tool/get_chart_data.py
+++ b/superset/mcp_service/chart/tool/get_chart_data.py
@@ -462,6 +462,7 @@ async def get_chart_data( # noqa: C901
# Execute the query
with event_logger.log_context(action="mcp.get_chart_data.query_execution"):
command = ChartDataCommand(query_context)
+ command.validate()
result = command.run()
# Handle empty query results for certain chart types
diff --git a/superset/mcp_service/chart/tool/get_chart_preview.py b/superset/mcp_service/chart/tool/get_chart_preview.py
index ee7c70a481b..6ee3ece4df4 100644
--- a/superset/mcp_service/chart/tool/get_chart_preview.py
+++ b/superset/mcp_service/chart/tool/get_chart_preview.py
@@ -160,6 +160,7 @@ class ASCIIPreviewStrategy(PreviewFormatStrategy):
)
command = ChartDataCommand(query_context)
+ command.validate()
result = command.run()
data = []
@@ -234,6 +235,7 @@ class TablePreviewStrategy(PreviewFormatStrategy):
)
command = ChartDataCommand(query_context)
+ command.validate()
result = command.run()
data = []
@@ -340,6 +342,7 @@ class VegaLitePreviewStrategy(PreviewFormatStrategy):
# Execute the query
command = ChartDataCommand(query_context)
+ command.validate()
result = command.run()
# Extract data from result
diff --git a/tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py b/tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
index 7850ef82a9a..0230d5edcf8 100644
--- a/tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
+++ b/tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
@@ -671,3 +671,190 @@ class TestGetChartDataRequestSchema:
assert data["identifier"] == 123
assert data["limit"] == 50
assert data["format"] == "json"
+
+
+class TestChartDataCommandValidation:
+ """Tests that ChartDataCommand.validate() is called before run().
+
+ These tests verify the security fix (CWE-862) that adds command.validate()
+ before command.run() in MCP chart data tools. The validate() call enforces
+ row-level security, guest user guards, and schema-level permissions.
+ """
+
+ def test_preview_utils_calls_validate_before_run(self):
+ """Test that generate_preview_from_form_data calls validate() before run()."""
+ from unittest.mock import MagicMock, patch
+
+ call_order: list[str] = []
+ mock_query_result = {"queries": [{"data": [{"col1": "a", "col2": 1}]}]}
+
+ mock_command = MagicMock()
+ mock_command.validate.side_effect = lambda: call_order.append("validate")
+ mock_command.run.side_effect = lambda: (
+ call_order.append("run"),
+ mock_query_result,
+ )[1]
+
+ mock_dataset = MagicMock()
+ mock_dataset.id = 10
+
+ # ChartDataCommand is module-level import in preview_utils;
+ # db and QueryContextFactory are local imports inside the function.
+ with (
+ patch("superset.extensions.db") as mock_db,
+ patch(
+ "superset.mcp_service.chart.preview_utils.ChartDataCommand",
+ return_value=mock_command,
+ ),
+ patch(
+ "superset.common.query_context_factory.QueryContextFactory"
+ ) as mock_factory,
+ ):
+ mock_db.session.query.return_value.get.return_value = mock_dataset
+ mock_factory.return_value.create.return_value = MagicMock()
+
+ from superset.mcp_service.chart.preview_utils import (
+ generate_preview_from_form_data,
+ )
+
+ generate_preview_from_form_data(
+ form_data={"metrics": [{"label": "count"}], "viz_type": "table"},
+ dataset_id=10,
+ preview_format="table",
+ )
+
+ mock_command.validate.assert_called_once()
+ mock_command.run.assert_called_once()
+ assert call_order == ["validate", "run"]
+
+ def test_preview_utils_security_exception_from_validate(self):
+ """Test that SupersetSecurityException from validate() is propagated."""
+ from unittest.mock import MagicMock, patch
+
+ from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
+ from superset.exceptions import SupersetSecurityException
+ from superset.mcp_service.chart.schemas import ChartError
+
+ security_error = SupersetSecurityException(
+ SupersetError(
+ message="Access denied",
+ error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
+ level=ErrorLevel.ERROR,
+ )
+ )
+
+ mock_command = MagicMock()
+ mock_command.validate.side_effect = security_error
+
+ mock_dataset = MagicMock()
+ mock_dataset.id = 10
+
+ with (
+ patch("superset.extensions.db") as mock_db,
+ patch(
+ "superset.mcp_service.chart.preview_utils.ChartDataCommand",
+ return_value=mock_command,
+ ),
+ patch(
+ "superset.common.query_context_factory.QueryContextFactory"
+ ) as mock_factory,
+ ):
+ mock_db.session.query.return_value.get.return_value = mock_dataset
+ mock_factory.return_value.create.return_value = MagicMock()
+
+ from superset.mcp_service.chart.preview_utils import (
+ generate_preview_from_form_data,
+ )
+
+ result = generate_preview_from_form_data(
+ form_data={"metrics": [{"label": "count"}], "viz_type": "table"},
+ dataset_id=10,
+ preview_format="table",
+ )
+
+ # SupersetSecurityException is caught by the broad except and
+ # returned as a ChartError
+ assert isinstance(result, ChartError)
+ assert "Access denied" in result.error
+ mock_command.run.assert_not_called()
+
+ def test_compile_chart_calls_validate_before_run(self):
+ """Test that _compile_chart calls validate() before run()."""
+ from unittest.mock import MagicMock, patch
+
+ call_order: list[str] = []
+ mock_query_result = {"queries": [{"data": [{"col1": 1}]}]}
+
+ mock_command = MagicMock()
+ mock_command.validate.side_effect = lambda: call_order.append("validate")
+ mock_command.run.side_effect = lambda: (
+ call_order.append("run"),
+ mock_query_result,
+ )[1]
+
+ # Both ChartDataCommand and QueryContextFactory are local imports
+ # inside _compile_chart, so patch at source.
+ with (
+ patch(
+ "superset.commands.chart.data.get_data_command.ChartDataCommand",
+ return_value=mock_command,
+ ),
+ patch(
+ "superset.common.query_context_factory.QueryContextFactory"
+ ) as mock_factory,
+ ):
+ mock_factory.return_value.create.return_value = MagicMock()
+
+ from superset.mcp_service.chart.tool.generate_chart import _compile_chart
+
+ result = _compile_chart(
+ form_data={"metrics": [{"label": "count"}], "viz_type": "table"},
+ dataset_id=10,
+ )
+
+ assert result.success is True
+ mock_command.validate.assert_called_once()
+ mock_command.run.assert_called_once()
+ assert call_order == ["validate", "run"]
+
+ def test_compile_chart_security_exception_from_validate(self):
+ """Test that _compile_chart propagates security exception from validate()."""
+ from unittest.mock import MagicMock, patch
+
+ from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
+ from superset.exceptions import SupersetSecurityException
+
+ security_error = SupersetSecurityException(
+ SupersetError(
+ message="Row-level security violation",
+ error_type=SupersetErrorType.DATASOURCE_SECURITY_ACCESS_ERROR,
+ level=ErrorLevel.ERROR,
+ )
+ )
+
+ mock_command = MagicMock()
+ mock_command.validate.side_effect = security_error
+
+ with (
+ patch(
+ "superset.commands.chart.data.get_data_command.ChartDataCommand",
+ return_value=mock_command,
+ ),
+ patch(
+ "superset.common.query_context_factory.QueryContextFactory"
+ ) as mock_factory,
+ ):
+ mock_factory.return_value.create.return_value = MagicMock()
+
+ from superset.mcp_service.chart.tool.generate_chart import _compile_chart
+
+ # SupersetSecurityException is not caught by _compile_chart's
+ # specific except blocks, so it propagates to the caller
+ # (generate_chart's broad except handler).
+ with pytest.raises(SupersetSecurityException):
+ _compile_chart(
+ form_data={"metrics": [{"label": "count"}], "viz_type": "table"},
+ dataset_id=10,
+ )
+
+ mock_command.run.assert_not_called()
(superset) 09/09: fix(mcp): improve default chart names with descriptive format (#38406)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit a9b24da0a2e353fb15db6afdbe99fde02a244206
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Tue Mar 10 10:53:05 2026 +0100
fix(mcp): improve default chart names with descriptive format (#38406)
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
(cherry picked from commit 0cfd760a3657c1f78474bba52fc19fd49170f57d)
---
superset/mcp_service/chart/chart_utils.py | 157 +++++++++++++++++++--
superset/mcp_service/chart/tool/generate_chart.py | 13 +-
.../mcp_service/chart/test_chart_utils.py | 131 +++++++++++++++--
.../mcp_service/chart/test_new_chart_types.py | 8 +-
4 files changed, 279 insertions(+), 30 deletions(-)
diff --git a/superset/mcp_service/chart/chart_utils.py b/superset/mcp_service/chart/chart_utils.py
index 600252f2cfa..f0bc771ceb7 100644
--- a/superset/mcp_service/chart/chart_utils.py
+++ b/superset/mcp_service/chart/chart_utils.py
@@ -789,34 +789,165 @@ def map_filter_operator(op: str) -> str:
return operator_map.get(op, op)
+def _humanize_column(col: ColumnRef) -> str:
+ """Return a human-readable label for a column reference."""
+ if col.label:
+ return col.label
+ name = col.name.replace("_", " ").title()
+ if col.aggregate:
+ return f"{col.aggregate.capitalize()}({name})"
+ return name
+
+
+def _summarize_filters(
+ filters: list[Any] | None,
+) -> str | None:
+ """Extract a short context string from filter configs."""
+ if not filters:
+ return None
+ parts: list[str] = []
+ for f in filters[:2]:
+ col = getattr(f, "column", "")
+ val = getattr(f, "value", "")
+ if isinstance(val, list):
+ val = ", ".join(str(v) for v in val[:3])
+ parts.append(f"{str(col).replace('_', ' ').title()} {val}")
+ return ", ".join(parts) if parts else None
+
+
+def _truncate(name: str, max_length: int = 60) -> str:
+ """Truncate to *max_length*, preserving the en-dash context portion."""
+ if len(name) <= max_length:
+ return name
+ if " \u2013 " in name:
+ what, _context = name.split(" \u2013 ", 1)
+ if len(what) <= max_length:
+ return what
+ return name[: max_length - 1] + "\u2026"
+
+
+def _table_chart_what(config: TableChartConfig, dataset_name: str | None) -> str:
+ """Build the descriptive fragment for a table chart."""
+ has_agg = any(col.aggregate for col in config.columns)
+ if has_agg:
+ metrics = [col for col in config.columns if col.aggregate]
+ what = ", ".join(_humanize_column(m) for m in metrics[:2])
+ return f"{what} Summary"
+ if dataset_name:
+ return f"{dataset_name} Records"
+ cols = ", ".join(_humanize_column(c) for c in config.columns[:3])
+ return f"{cols} Table"
+
+
+def _xy_chart_what(config: XYChartConfig) -> str:
+ """Build the descriptive fragment for an XY chart."""
+ primary_metric = _humanize_column(config.y[0]) if config.y else "Value"
+ dimension = _humanize_column(config.x)
+
+ if config.kind in ("line", "area") and config.group_by is None:
+ return f"{primary_metric} Over Time"
+ if config.group_by is not None:
+ group_label = _humanize_column(config.group_by)
+ return f"{primary_metric} by {group_label}"
+ if config.kind == "scatter":
+ return f"{primary_metric} vs {dimension}"
+ return f"{primary_metric} by {dimension}"
+
+
+_GRAIN_MAP: dict[str, str] = {
+ "PT1H": "Hourly",
+ "P1D": "Daily",
+ "P1W": "Weekly",
+ "P1M": "Monthly",
+ "P3M": "Quarterly",
+ "P1Y": "Yearly",
+}
+
+
+def _xy_chart_context(config: XYChartConfig) -> str | None:
+ """Build context (time grain / filters) for an XY chart name."""
+ parts: list[str] = []
+ if config.time_grain:
+ grain_val = (
+ config.time_grain.value
+ if hasattr(config.time_grain, "value")
+ else str(config.time_grain)
+ )
+ grain_str = _GRAIN_MAP.get(grain_val, grain_val)
+ parts.append(grain_str)
+ if filter_ctx := _summarize_filters(config.filters):
+ parts.append(filter_ctx)
+ return ", ".join(parts) if parts else None
+
+
+def _pie_chart_what(config: PieChartConfig) -> str:
+ """Build the 'what' portion for a pie chart name."""
+ dim = config.dimension.name
+ metric_label = config.metric.label or config.metric.name
+ return f"{dim} by {metric_label}"
+
+
+def _pivot_table_what(config: PivotTableChartConfig) -> str:
+ """Build the 'what' portion for a pivot table chart name."""
+ row_names = ", ".join(r.name for r in config.rows)
+ return f"Pivot Table \u2013 {row_names}"
+
+
+def _mixed_timeseries_what(config: MixedTimeseriesChartConfig) -> str:
+ """Build the 'what' portion for a mixed timeseries chart name."""
+ primary = config.y[0].label or config.y[0].name if config.y else "primary"
+ secondary = (
+ config.y_secondary[0].label or config.y_secondary[0].name
+ if config.y_secondary
+ else "secondary"
+ )
+ return f"{primary} + {secondary}"
+
+
def generate_chart_name(
config: TableChartConfig
| XYChartConfig
| PieChartConfig
| PivotTableChartConfig
| MixedTimeseriesChartConfig,
+ dataset_name: str | None = None,
) -> str:
- """Generate a chart name based on the configuration."""
+ """Generate a descriptive chart name following a standard format.
+
+ Format conventions (by chart type):
+ Aggregated (bar/scatter with group_by): [Metric] by [Dimension]
+ Time-series (line/area, no group_by): [Metric] Over Time
+ Table (no aggregates): [Dataset] Records
+ Table (with aggregates): [Metric] Summary
+ Pie: [Dimension] by [Metric]
+ Pivot Table: Pivot Table – [Row1, Row2]
+ Mixed Timeseries: [Primary] + [Secondary]
+ An en-dash followed by context (filters / time grain) is appended
+ when such information is available.
+ """
if isinstance(config, TableChartConfig):
- return f"Table Chart - {', '.join(col.name for col in config.columns)}"
+ what = _table_chart_what(config, dataset_name)
+ context = _summarize_filters(config.filters)
elif isinstance(config, XYChartConfig):
- chart_type = config.kind.capitalize()
- x_col = config.x.name
- y_cols = ", ".join(col.name for col in config.y)
- return f"{chart_type} Chart - {x_col} vs {y_cols}"
+ what = _xy_chart_what(config)
+ context = _xy_chart_context(config)
elif isinstance(config, PieChartConfig):
- metric_label = config.metric.label or config.metric.name
- return f"Pie Chart - {config.dimension.name} by {metric_label}"
+ what = _pie_chart_what(config)
+ context = _summarize_filters(config.filters)
elif isinstance(config, PivotTableChartConfig):
- rows = ", ".join(col.name for col in config.rows)
- return f"Pivot Table - {rows}"
+ what = _pivot_table_what(config)
+ context = _summarize_filters(config.filters)
elif isinstance(config, MixedTimeseriesChartConfig):
- primary = ", ".join(col.name for col in config.y)
- secondary = ", ".join(col.name for col in config.y_secondary)
- return f"Mixed Chart - {primary} + {secondary}"
+ what = _mixed_timeseries_what(config)
+ context = _summarize_filters(config.filters)
else:
return "Chart"
+ name = what
+ if context:
+ name = f"{what} \u2013 {context}"
+ return _truncate(name)
+
def analyze_chart_capabilities(chart: Any | None, config: Any) -> ChartCapabilities:
"""Analyze chart capabilities based on type and configuration."""
diff --git a/superset/mcp_service/chart/tool/generate_chart.py b/superset/mcp_service/chart/tool/generate_chart.py
index b5711d5e5f7..f09d8f89fdf 100644
--- a/superset/mcp_service/chart/tool/generate_chart.py
+++ b/superset/mcp_service/chart/tool/generate_chart.py
@@ -264,10 +264,6 @@ async def generate_chart( # noqa: C901
await ctx.report_progress(2, 5, "Creating chart in database")
from superset.commands.chart.create import CreateChartCommand
- # Use custom chart name if provided, otherwise auto-generate
- chart_name = request.chart_name or generate_chart_name(request.config)
- await ctx.debug("Chart name: chart_name=%s" % (chart_name,))
-
# Find the dataset to get its numeric ID
from superset.daos.dataset import DatasetDAO
@@ -344,6 +340,15 @@ async def generate_chart( # noqa: C901
}
)
+ # Generate chart name after dataset lookup so we can include dataset name
+ dataset_name = getattr(dataset, "datasource_name", None) or getattr(
+ dataset, "table_name", None
+ )
+ chart_name = request.chart_name or generate_chart_name(
+ request.config, dataset_name=dataset_name
+ )
+ await ctx.debug("Chart name: chart_name=%s" % (chart_name,))
+
try:
with event_logger.log_context(action="mcp.generate_chart.db_write"):
command = CreateChartCommand(
diff --git a/tests/unit_tests/mcp_service/chart/test_chart_utils.py b/tests/unit_tests/mcp_service/chart/test_chart_utils.py
index c142aabb8ba..52f1b26e649 100644
--- a/tests/unit_tests/mcp_service/chart/test_chart_utils.py
+++ b/tests/unit_tests/mcp_service/chart/test_chart_utils.py
@@ -490,8 +490,8 @@ class TestMapConfigToFormData:
class TestGenerateChartName:
"""Test generate_chart_name function"""
- def test_generate_table_chart_name(self) -> None:
- """Test generating name for table chart"""
+ def test_table_no_aggregates(self) -> None:
+ """Table without aggregates uses column names."""
config = TableChartConfig(
chart_type="table",
columns=[
@@ -501,25 +501,138 @@ class TestGenerateChartName:
)
result = generate_chart_name(config)
- assert result == "Table Chart - product, revenue"
+ assert result == "Product, Revenue Table"
- def test_generate_xy_chart_name(self) -> None:
- """Test generating name for XY chart"""
+ def test_table_no_aggregates_with_dataset_name(self) -> None:
+ """Table without aggregates includes dataset name when available."""
+ config = TableChartConfig(
+ chart_type="table",
+ columns=[ColumnRef(name="product")],
+ )
+
+ result = generate_chart_name(config, dataset_name="Orders")
+ assert result == "Orders Records"
+
+ def test_table_with_aggregates(self) -> None:
+ """Table with aggregates produces a summary name."""
+ config = TableChartConfig(
+ chart_type="table",
+ columns=[
+ ColumnRef(name="product"),
+ ColumnRef(name="revenue", aggregate="SUM"),
+ ],
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Sum(Revenue) Summary"
+
+ def test_line_chart_over_time(self) -> None:
+ """Line chart without group_by uses 'Over Time' format."""
+ config = XYChartConfig(
+ chart_type="xy",
+ x=ColumnRef(name="order_date"),
+ y=[ColumnRef(name="revenue", aggregate="SUM")],
+ kind="line",
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Sum(Revenue) Over Time"
+
+ def test_bar_chart_by_dimension(self) -> None:
+ """Bar chart uses 'by [X]' format."""
+ config = XYChartConfig(
+ chart_type="xy",
+ x=ColumnRef(name="product_category"),
+ y=[ColumnRef(name="order_count", aggregate="COUNT")],
+ kind="bar",
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Count(Order Count) by Product Category"
+
+ def test_line_chart_with_group_by(self) -> None:
+ """Line chart with group_by uses 'by [group]' format."""
+ config = XYChartConfig(
+ chart_type="xy",
+ x=ColumnRef(name="date"),
+ y=[ColumnRef(name="revenue", aggregate="SUM")],
+ kind="line",
+ group_by=ColumnRef(name="sales_rep"),
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Sum(Revenue) by Sales Rep"
+
+ def test_scatter_plot(self) -> None:
+ """Scatter plot uses 'Y vs X' format."""
+ config = XYChartConfig(
+ chart_type="xy",
+ x=ColumnRef(name="age"),
+ y=[ColumnRef(name="income")],
+ kind="scatter",
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Income vs Age"
+
+ def test_time_grain_in_context(self) -> None:
+ """Time grain is appended as context."""
config = XYChartConfig(
chart_type="xy",
x=ColumnRef(name="date"),
- y=[ColumnRef(name="revenue"), ColumnRef(name="orders")],
+ y=[ColumnRef(name="revenue", aggregate="SUM")],
kind="line",
+ time_grain="P1M",
)
result = generate_chart_name(config)
- assert result == "Line Chart - date vs revenue, orders"
+ assert result == "Sum(Revenue) Over Time \u2013 Monthly"
+
+ def test_filter_context(self) -> None:
+ """Filters are appended as context."""
+ config = TableChartConfig(
+ chart_type="table",
+ columns=[ColumnRef(name="product")],
+ filters=[FilterConfig(column="region", op="=", value="West")],
+ )
+
+ result = generate_chart_name(config, dataset_name="Orders")
+ assert result == "Orders Records \u2013 Region West"
- def test_generate_chart_name_unsupported(self) -> None:
- """Test generating name for unsupported config type"""
+ def test_name_truncation(self) -> None:
+ """Names exceeding 60 chars are truncated."""
+ config = XYChartConfig(
+ chart_type="xy",
+ x=ColumnRef(name="date"),
+ y=[
+ ColumnRef(
+ name="very_long_metric_name_that_goes_on_and_on", aggregate="SUM"
+ )
+ ],
+ kind="line",
+ group_by=ColumnRef(name="another_very_long_dimension_name_here"),
+ )
+
+ result = generate_chart_name(config)
+ assert len(result) <= 60
+
+ def test_unsupported_config_type(self) -> None:
+ """Unsupported config type returns generic name."""
result = generate_chart_name("invalid_config") # type: ignore
assert result == "Chart"
+ def test_custom_labels_used(self) -> None:
+ """Column labels are preferred over names."""
+ config = XYChartConfig(
+ chart_type="xy",
+ x=ColumnRef(name="ds", label="Date"),
+ y=[ColumnRef(name="cnt", aggregate="COUNT", label="Order Count")],
+ kind="bar",
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Order Count by Date"
+
class TestGenerateExploreLink:
"""Test generate_explore_link function"""
diff --git a/tests/unit_tests/mcp_service/chart/test_new_chart_types.py b/tests/unit_tests/mcp_service/chart/test_new_chart_types.py
index e930ab83091..e2e37ba99e6 100644
--- a/tests/unit_tests/mcp_service/chart/test_new_chart_types.py
+++ b/tests/unit_tests/mcp_service/chart/test_new_chart_types.py
@@ -723,7 +723,7 @@ class TestGenerateChartNameNewTypes:
metric=ColumnRef(name="revenue", aggregate="SUM"),
)
result = generate_chart_name(config)
- assert result == "Pie Chart - product by revenue"
+ assert result == "product by revenue"
def test_pie_chart_name_with_custom_label(self) -> None:
config = PieChartConfig(
@@ -732,7 +732,7 @@ class TestGenerateChartNameNewTypes:
metric=ColumnRef(name="revenue", aggregate="SUM", label="Total Revenue"),
)
result = generate_chart_name(config)
- assert result == "Pie Chart - product by Total Revenue"
+ assert result == "product by Total Revenue"
def test_pivot_table_chart_name(self) -> None:
config = PivotTableChartConfig(
@@ -741,7 +741,7 @@ class TestGenerateChartNameNewTypes:
metrics=[ColumnRef(name="revenue", aggregate="SUM")],
)
result = generate_chart_name(config)
- assert result == "Pivot Table - product, region"
+ assert result == "Pivot Table \u2013 product, region"
def test_mixed_timeseries_chart_name(self) -> None:
config = MixedTimeseriesChartConfig(
@@ -751,7 +751,7 @@ class TestGenerateChartNameNewTypes:
y_secondary=[ColumnRef(name="orders", aggregate="COUNT")],
)
result = generate_chart_name(config)
- assert result == "Mixed Chart - revenue + orders"
+ assert result == "revenue + orders"
# ============================================================
(superset) 07/09: fix(mcp): add guardrails to prevent LLM artifact generation (#38391)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit d266146bbb9ef5614cff0341676ce1ddadcf78df
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Tue Mar 10 09:51:23 2026 +0100
fix(mcp): add guardrails to prevent LLM artifact generation (#38391)
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
(cherry picked from commit 5fa70bdbd822be025bacae01c8e3c3508b38c3bc)
---
superset/mcp_service/app.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/superset/mcp_service/app.py b/superset/mcp_service/app.py
index a26167151f0..43d4723fb94 100644
--- a/superset/mcp_service/app.py
+++ b/superset/mcp_service/app.py
@@ -156,6 +156,19 @@ CRITICAL RULES - NEVER VIOLATE:
- Parameter name reminders: open_sql_lab_with_context uses "sql" (not "query"),
execute_sql uses "sql" (not "query").
+IMPORTANT - Tool-Only Interaction:
+- Do NOT generate code artifacts, HTML pages, JavaScript snippets, or any code intended
+ for the user to run. All visualization, data retrieval, and authentication are handled
+ by the provided MCP tools.
+- Always call the appropriate tool directly instead of writing code. For example, use
+ generate_chart to create visualizations rather than generating plotting code.
+- When a tool returns a URL (chart URL, dashboard URL, explore link, SQL Lab link),
+ return that URL to the user. Do NOT attempt to recreate the visualization in code.
+- Do NOT generate HTML dashboards, embed scripts, or custom frontend code. Use
+ generate_dashboard and add_chart_to_existing_dashboard for dashboard operations.
+- If a user asks for something the tools cannot do, explain the limitation and suggest
+ the closest available tool rather than generating code as a workaround.
+
General usage tips:
- All listing tools use 1-based pagination (first page is 1)
- Use get_schema to discover filterable columns, sortable columns, and default columns
(superset) 01/09: fix(docs): swizzle MethodEndpoint to fix SSG crash on all API pages (#38533)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 6ebaf5919f2f3e999d48ca33731d64f6d9d99d7f
Author: Evan Rusackas <ev...@preset.io>
AuthorDate: Mon Mar 9 17:18:59 2026 -0400
fix(docs): swizzle MethodEndpoint to fix SSG crash on all API pages (#38533)
Co-authored-by: Superset Dev <de...@superset.apache.org>
Co-authored-by: Claude Sonnet 4.6 <no...@anthropic.com>
(cherry picked from commit 8e3e57c1c827331d2a048182eaca9ac8d4732998)
---
.../src/theme/ApiExplorer/MethodEndpoint/index.tsx | 120 +++++++++++++++++++++
1 file changed, 120 insertions(+)
diff --git a/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx b/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
new file mode 100644
index 00000000000..276331db547
--- /dev/null
+++ b/docs/src/theme/ApiExplorer/MethodEndpoint/index.tsx
@@ -0,0 +1,120 @@
+/**
+ * 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.
+ *
+ * Swizzled from docusaurus-theme-openapi-docs to fix SSG crash.
+ *
+ * The original component calls useTypedSelector (Redux) at the top level,
+ * which fails during static site generation because no Redux store is
+ * available. This version moves the hook into a browser-only child component
+ * so SSG can render the page without a store context.
+ */
+
+import React from "react";
+
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import { useSelector } from "react-redux";
+
+interface ServerVariable {
+ default?: string;
+}
+
+interface ServerValue {
+ url: string;
+ variables?: Record<string, ServerVariable>;
+}
+
+interface StoreState {
+ server: { value: ServerValue | null };
+}
+
+function colorForMethod(method: string) {
+ switch (method.toLowerCase()) {
+ case "get":
+ return "primary";
+ case "post":
+ return "success";
+ case "delete":
+ return "danger";
+ case "put":
+ return "info";
+ case "patch":
+ return "warning";
+ case "head":
+ return "secondary";
+ case "event":
+ return "secondary";
+ default:
+ return undefined;
+ }
+}
+
+export interface Props {
+ method: string;
+ path: string;
+ context?: "endpoint" | "callback";
+}
+
+// Inner component rendered only in the browser, where the Redux store exists.
+function ServerUrl() {
+ const serverValue = useSelector((state: StoreState) => state.server.value);
+
+ if (serverValue && serverValue.variables) {
+ let serverUrlWithVariables = serverValue.url.replace(/\/$/, "");
+ Object.keys(serverValue.variables).forEach((variable) => {
+ serverUrlWithVariables = serverUrlWithVariables.replace(
+ `{${variable}}`,
+ serverValue.variables?.[variable].default ?? ""
+ );
+ });
+ return <>{serverUrlWithVariables}</>;
+ }
+
+ if (serverValue && serverValue.url) {
+ return <>{serverValue.url}</>;
+ }
+
+ return null;
+}
+
+function MethodEndpoint({ method, path, context }: Props) {
+ const renderServerUrl = () => {
+ if (context === "callback") {
+ return "";
+ }
+ return <BrowserOnly>{() => <ServerUrl />}</BrowserOnly>;
+ };
+
+ return (
+ <>
+ <pre className="openapi__method-endpoint">
+ <span className={"badge badge--" + colorForMethod(method)}>
+ {method === "event" ? "Webhook" : method.toUpperCase()}
+ </span>{" "}
+ {method !== "event" && (
+ <h2 className="openapi__method-endpoint-path">
+ {renderServerUrl()}
+ {`${path.replace(/{([a-z0-9-_]+)}/gi, ":$1")}`}
+ </h2>
+ )}
+ </pre>
+ <div className="openapi__divider" />
+ </>
+ );
+}
+
+export default MethodEndpoint;
(superset) 03/09: fix(mcp): resolve chatbot tool call flakiness with URL and instruction fixes (#38532)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 8d7a36df5ae2d5a5cb7257b34ce46c92aeee8818
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Mon Mar 9 23:35:50 2026 +0100
fix(mcp): resolve chatbot tool call flakiness with URL and instruction fixes (#38532)
(cherry picked from commit 6ef4794778b4543c09acd7e1a4db57809e5b7238)
---
superset/mcp_service/app.py | 14 ++++++++++++++
.../mcp_service/sql_lab/tool/open_sql_lab_with_context.py | 3 ++-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/superset/mcp_service/app.py b/superset/mcp_service/app.py
index dd48c0a6766..a26167151f0 100644
--- a/superset/mcp_service/app.py
+++ b/superset/mcp_service/app.py
@@ -142,6 +142,20 @@ Query Examples:
- My dashboards:
filters=[{{"col": "created_by_fk", "opr": "eq", "value": <user_id>}}]
+To modify an existing chart (add filters, change metrics, change dimensions, etc.):
+1. get_chart_info(chart_id) -> examine current configuration
+2. update_chart(chart_id, config) -> apply changes (filters, metrics, dimensions)
+Do NOT use execute_sql for chart modifications. Use update_chart instead.
+
+CRITICAL RULES - NEVER VIOLATE:
+- NEVER fabricate or invent URLs. ALL URLs must come from tool call results.
+ If you need a link, call the appropriate tool (generate_explore_link, generate_chart,
+ open_sql_lab_with_context, etc.) and use the URL it returns.
+- To modify an existing chart's filters, metrics, or dimensions, use update_chart.
+ Do NOT use execute_sql for chart modifications.
+- Parameter name reminders: open_sql_lab_with_context uses "sql" (not "query"),
+ execute_sql uses "sql" (not "query").
+
General usage tips:
- All listing tools use 1-based pagination (first page is 1)
- Use get_schema to discover filterable columns, sortable columns, and default columns
diff --git a/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py b/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py
index cfedc72bd93..d27b970d609 100644
--- a/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py
+++ b/superset/mcp_service/sql_lab/tool/open_sql_lab_with_context.py
@@ -33,6 +33,7 @@ from superset.mcp_service.sql_lab.schemas import (
SqlLabResponse,
)
from superset.mcp_service.utils.schema_utils import parse_request
+from superset.mcp_service.utils.url_utils import get_superset_base_url
logger = logging.getLogger(__name__)
@@ -95,7 +96,7 @@ def open_sql_lab_with_context(
# Construct SQL Lab URL
query_string = urlencode(params)
- url = f"/sqllab?{query_string}"
+ url = f"{get_superset_base_url()}/sqllab?{query_string}"
logger.info(
"Generated SQL Lab URL for database %s", request.database_connection_id
(superset) 05/09: feat(mcp): register GlobalErrorHandlerMiddleware and LoggingMiddleware (#38523)
Posted by mi...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
michaelsmolina pushed a commit to branch 6.1
in repository https://gitbox.apache.org/repos/asf/superset.git
commit d42caf744fa015f8e9ea67fddb7d9b35e3754d85
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Tue Mar 10 09:48:38 2026 +0100
feat(mcp): register GlobalErrorHandlerMiddleware and LoggingMiddleware (#38523)
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
(cherry picked from commit 0533ca9941e9d3535a6647613e8832e094b49777)
---
superset/mcp_service/server.py | 22 +++++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py
index f2ee1d69a0d..5435a66e68b 100644
--- a/superset/mcp_service/server.py
+++ b/superset/mcp_service/server.py
@@ -30,7 +30,11 @@ import uvicorn
from superset.mcp_service.app import create_mcp_app, init_fastmcp_server
from superset.mcp_service.mcp_config import get_mcp_factory_config, MCP_STORE_CONFIG
-from superset.mcp_service.middleware import create_response_size_guard_middleware
+from superset.mcp_service.middleware import (
+ create_response_size_guard_middleware,
+ GlobalErrorHandlerMiddleware,
+ LoggingMiddleware,
+)
from superset.mcp_service.storage import _create_redis_store
logger = logging.getLogger(__name__)
@@ -224,16 +228,24 @@ def run_server(
auth_provider = _create_auth_provider(flask_app)
# Build middleware list
+ # FastMCP wraps handlers so that the LAST-added middleware is
+ # outermost. Order here is innermost → outermost.
middleware_list = []
+ # Add caching middleware (innermost – runs closest to the tool)
+ caching_middleware = create_response_caching_middleware()
+ if caching_middleware:
+ middleware_list.append(caching_middleware)
+
# Add response size guard (protects LLM clients from huge responses)
if size_guard_middleware := create_response_size_guard_middleware():
middleware_list.append(size_guard_middleware)
- # Add caching middleware
- caching_middleware = create_response_caching_middleware()
- if caching_middleware:
- middleware_list.append(caching_middleware)
+ # Add logging middleware (logs all tool calls with duration tracking)
+ middleware_list.append(LoggingMiddleware())
+
+ # Add global error handler (outermost – catches all exceptions)
+ middleware_list.append(GlobalErrorHandlerMiddleware())
mcp_instance = init_fastmcp_server(
auth=auth_provider,