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/09/29 05:04:58 UTC

(superset) branch master updated: fix(mcp): hide internal sanitization warnings from input schemas (#44602)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 19ea6b231c7 fix(mcp): hide internal sanitization warnings from input schemas (#44602)
19ea6b231c7 is described below

commit 19ea6b231c7e4a70adb4b0ffe0ea18aec6dcf783
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Tue Sep 29 15:04:50 2026 +1000

    fix(mcp): hide internal sanitization warnings from input schemas (#44602)
---
 docs/developer_docs/extensions/mcp.md              |  4 ++
 superset/mcp_service/chart/schemas.py              |  3 +-
 superset/mcp_service/dashboard/schemas.py          |  7 +-
 .../dashboard/tool/test_duplicate_dashboard.py     | 21 +++++-
 .../test_sanitization_warning_inputs.py            | 80 ++++++++++++++++++++++
 5 files changed, 110 insertions(+), 5 deletions(-)

diff --git a/docs/developer_docs/extensions/mcp.md b/docs/developer_docs/extensions/mcp.md
index 6d54f1ec0ca..84c02557eb0 100644
--- a/docs/developer_docs/extensions/mcp.md
+++ b/docs/developer_docs/extensions/mcp.md
@@ -44,6 +44,10 @@ Tools are Python functions that AI agents can call to perform specific tasks. Th
 - Specialized report generation
 - Business-specific operations
 
+Sanitization notices are returned in tool response `warnings`. The internal
+`sanitization_warnings` request attribute is not advertised in input schemas;
+caller-supplied values are discarded rather than echoed in responses.
+
 ### MCP Prompts
 
 Prompts provide interactive guidance and context to AI agents. They help agents understand how to better assist users with specific workflows or domain knowledge.
diff --git a/superset/mcp_service/chart/schemas.py b/superset/mcp_service/chart/schemas.py
index ada93209cab..b3b10a53e42 100644
--- a/superset/mcp_service/chart/schemas.py
+++ b/superset/mcp_service/chart/schemas.py
@@ -41,6 +41,7 @@ from pydantic import (
     StrictBool,
     ValidationError,
 )
+from pydantic.json_schema import SkipJsonSchema
 from typing_extensions import Self, TypedDict
 
 from superset.constants import NO_TIME_RANGE, TimeGrain
@@ -3840,7 +3841,7 @@ class GenerateChartRequest(ChartRequestNormalizerMixin, QueryCacheControl):
     preview_formats: List[Literal["url", "ascii", "vega_lite", "table"]] = Field(
         default_factory=lambda: ["url"],
     )
-    sanitization_warnings: List[str] = Field(
+    sanitization_warnings: SkipJsonSchema[List[str]] = Field(
         default_factory=list,
         description=(
             "Internal: warnings emitted when user input was altered by "
diff --git a/superset/mcp_service/dashboard/schemas.py b/superset/mcp_service/dashboard/schemas.py
index 8b7dfee325b..54cab9a0914 100644
--- a/superset/mcp_service/dashboard/schemas.py
+++ b/superset/mcp_service/dashboard/schemas.py
@@ -80,6 +80,7 @@ from pydantic import (
     model_serializer,
     model_validator,
 )
+from pydantic.json_schema import SkipJsonSchema
 
 if TYPE_CHECKING:
     from superset.connectors.sqla.models import SqlaTable
@@ -722,7 +723,7 @@ class GenerateDashboardRequest(BaseModel):
             "dashboard's css field."
         ),
     )
-    sanitization_warnings: List[str] = Field(
+    sanitization_warnings: SkipJsonSchema[List[str]] = Field(
         default_factory=list,
         description=(
             "Internal: warnings emitted when user input was altered by "
@@ -888,7 +889,7 @@ class UpdateDashboardRequest(BaseModel):
             "the ``filter_bar_orientation`` json_metadata key."
         ),
     )
-    sanitization_warnings: List[str] = Field(
+    sanitization_warnings: SkipJsonSchema[List[str]] = Field(
         default_factory=list,
         description=(
             "Internal: warnings emitted when user input was altered by "
@@ -1383,7 +1384,7 @@ class DuplicateDashboardRequest(BaseModel):
             "source."
         ),
     )
-    sanitization_warnings: List[str] = Field(
+    sanitization_warnings: SkipJsonSchema[List[str]] = Field(
         default_factory=list,
         description=(
             "Internal: warnings emitted when user input was altered by "
diff --git a/tests/unit_tests/mcp_service/dashboard/tool/test_duplicate_dashboard.py b/tests/unit_tests/mcp_service/dashboard/tool/test_duplicate_dashboard.py
index 5fffbc45f34..a1db92d3678 100644
--- a/tests/unit_tests/mcp_service/dashboard/tool/test_duplicate_dashboard.py
+++ b/tests/unit_tests/mcp_service/dashboard/tool/test_duplicate_dashboard.py
@@ -448,6 +448,7 @@ async def test_title_xss_is_sanitized(
                 "request": {
                     "dashboard_id": 1,
                     "dashboard_title": "<script>alert('x')</script>Regional Copy",
+                    "sanitization_warnings": ["caller-controlled warning"],
                 }
             },
         )
@@ -457,7 +458,9 @@ async def test_title_xss_is_sanitized(
     # The sanitized title — not the raw payload — is sent to the command.
     _, cmd_data = mock_copy_cmd_cls.call_args.args
     assert cmd_data["dashboard_title"] == "Regional Copy"
-    assert content["warnings"], "expected a sanitization warning"
+    assert len(content["warnings"]) == 1
+    assert "dashboard_title" in content["warnings"][0]
+    assert "caller-controlled" not in content["warnings"][0]
 
 
 @patch("superset.daos.dashboard.DashboardDAO.find_by_id")
@@ -692,3 +695,19 @@ def test_empty_title_rejected_by_schema() -> None:
 
     with pytest.raises(ValidationError):
         DuplicateDashboardRequest(dashboard_id=1, dashboard_title="")
+
+
+@pytest.mark.asyncio
+async def test_tool_input_schemas_omit_sanitization_warnings(
+    mcp_server: object,
+) -> None:
+    """Tool discovery must not advertise server-generated warning fields."""
+    async with Client(mcp_server) as client:
+        tools = {tool.name: tool for tool in await client.list_tools()}
+    for name in (
+        "generate_chart",
+        "generate_dashboard",
+        "update_dashboard",
+        "duplicate_dashboard",
+    ):
+        assert "sanitization_warnings" not in str(tools[name].inputSchema)
diff --git a/tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py b/tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py
new file mode 100644
index 00000000000..b7aa44cbc7a
--- /dev/null
+++ b/tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py
@@ -0,0 +1,80 @@
+# 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.
+
+"""Regression tests for server-only MCP sanitization warnings."""
+
+from typing import Any
+
+import pytest
+from pydantic import TypeAdapter
+
+from superset.mcp_service.chart.schemas import GenerateChartRequest
+from superset.mcp_service.dashboard.schemas import (
+    DuplicateDashboardRequest,
+    GenerateDashboardRequest,
+    UpdateDashboardRequest,
+)
+from superset.utils import json
+
+
+@pytest.mark.parametrize(
+    ("model", "payload", "title_field"),
+    [
+        (GenerateDashboardRequest, {"chart_ids": [1]}, "dashboard_title"),
+        (UpdateDashboardRequest, {"identifier": 1}, "dashboard_title"),
+        (DuplicateDashboardRequest, {"dashboard_id": 1}, "dashboard_title"),
+        (
+            GenerateChartRequest,
+            {
+                "dataset_id": 1,
+                "config": {"chart_type": "table", "columns": [{"name": "a"}]},
+            },
+            "chart_name",
+        ),
+    ],
+)
+@pytest.mark.parametrize("title", ["Safe title", "Safe <b>title</b>"])
+def test_sanitization_warnings_are_server_only(
+    model: type[
+        GenerateChartRequest
+        | GenerateDashboardRequest
+        | UpdateDashboardRequest
+        | DuplicateDashboardRequest
+    ],
+    payload: dict[str, Any],
+    title_field: str,
+    title: str,
+) -> None:
+    """Hide internal warnings in schemas without losing sanitization notices."""
+    assert "sanitization_warnings" not in json.dumps(model.model_json_schema())
+    # Also cover the type-adapter schema used by tool integrations.
+    assert "sanitization_warnings" not in json.dumps(TypeAdapter(model).json_schema())
+
+    payload = {
+        **payload,
+        title_field: title,
+        "sanitization_warnings": ["caller-controlled warning"],
+    }
+    request = model.model_validate_json(json.dumps(payload))
+    warnings = request.sanitization_warnings
+    assert getattr(request, title_field) == "Safe title"
+    if "<b>" in title:
+        assert len(warnings) == 1
+        assert title_field in warnings[0]
+        assert "caller-controlled" not in warnings[0]
+    else:
+        assert warnings == []