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/30 20:48:12 UTC

(superset) branch master updated: fix(mcp): enforce MAX_PAGE_SIZE limit on list tools to prevent oversized responses (#38959)

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

misterg 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 2c9cf0bd558 fix(mcp): enforce MAX_PAGE_SIZE limit on list tools to prevent oversized responses (#38959)
2c9cf0bd558 is described below

commit 2c9cf0bd558449805e458e02374c565f6c8270da
Author: Amin Ghadersohi <am...@gmail.com>
AuthorDate: Mon Mar 30 22:48:03 2026 +0200

    fix(mcp): enforce MAX_PAGE_SIZE limit on list tools to prevent oversized responses (#38959)
---
 superset/mcp_service/__main__.py                   | 32 ++++++++++++++++++++++
 superset/mcp_service/chart/schemas.py              |  9 +++++-
 superset/mcp_service/constants.py                  |  4 +++
 superset/mcp_service/dashboard/schemas.py          |  9 +++++-
 superset/mcp_service/dataset/schemas.py            |  9 +++++-
 superset/mcp_service/mcp_core.py                   |  5 ++++
 .../mcp_service/chart/tool/test_list_charts.py     | 14 ++++++++++
 7 files changed, 79 insertions(+), 3 deletions(-)

diff --git a/superset/mcp_service/__main__.py b/superset/mcp_service/__main__.py
index 0da7df6655e..30759c6e805 100644
--- a/superset/mcp_service/__main__.py
+++ b/superset/mcp_service/__main__.py
@@ -44,6 +44,36 @@ if os.environ.get("FASTMCP_TRANSPORT", "stdio") == "stdio":
 from superset.mcp_service.app import init_fastmcp_server, mcp
 
 
+def _add_default_middlewares() -> None:
+    """Add the standard middleware stack to the MCP instance.
+
+    This ensures all entry points (stdio, streamable-http, etc.) get
+    the same protection middlewares that the Flask CLI and server.py add.
+    Order is innermost → outermost (last-added wraps everything).
+    """
+    from superset.mcp_service.middleware import (
+        create_response_size_guard_middleware,
+        GlobalErrorHandlerMiddleware,
+        LoggingMiddleware,
+        StructuredContentStripperMiddleware,
+    )
+
+    # Response size guard (innermost among these)
+    if size_guard := create_response_size_guard_middleware():
+        mcp.add_middleware(size_guard)
+        limit = size_guard.token_limit
+        sys.stderr.write(f"[MCP] Response size guard enabled (token_limit={limit})\n")
+
+    # Logging
+    mcp.add_middleware(LoggingMiddleware())
+
+    # Global error handler
+    mcp.add_middleware(GlobalErrorHandlerMiddleware())
+
+    # Structured content stripper (must be outermost)
+    mcp.add_middleware(StructuredContentStripperMiddleware())
+
+
 def main() -> None:
     """
     Run the MCP service in stdio mode with proper output suppression.
@@ -97,6 +127,7 @@ def main() -> None:
             # Initialize the FastMCP server
             # Disable auth config for stdio mode to avoid Flask app output
             init_fastmcp_server()
+            _add_default_middlewares()
 
         # Log captured output to stderr for debugging (optional)
         captured = captured_output.getvalue()
@@ -118,6 +149,7 @@ def main() -> None:
     else:
         # For other transports, use normal initialization
         init_fastmcp_server()
+        _add_default_middlewares()
 
         # Run with specified transport
         if transport == "streamable-http":
diff --git a/superset/mcp_service/chart/schemas.py b/superset/mcp_service/chart/schemas.py
index 8914660c4fa..2da6c3ef804 100644
--- a/superset/mcp_service/chart/schemas.py
+++ b/superset/mcp_service/chart/schemas.py
@@ -46,6 +46,7 @@ from superset.mcp_service.common.cache_schemas import (
     QueryCacheControl,
 )
 from superset.mcp_service.common.error_schemas import ChartGenerationError
+from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
 from superset.mcp_service.system.schemas import (
     PaginationInfo,
     serialize_user_object,
@@ -1094,7 +1095,13 @@ class ListChartsRequest(MetadataCacheControl):
         Field(default=1, description="Page number for pagination (1-based)"),
     ]
     page_size: Annotated[
-        PositiveInt, Field(default=10, description="Number of items per page")
+        int,
+        Field(
+            default=DEFAULT_PAGE_SIZE,
+            gt=0,
+            le=MAX_PAGE_SIZE,
+            description=f"Number of items per page (max {MAX_PAGE_SIZE})",
+        ),
     ]
 
     @model_validator(mode="after")
diff --git a/superset/mcp_service/constants.py b/superset/mcp_service/constants.py
index 7abf91147a8..a23a7949e94 100644
--- a/superset/mcp_service/constants.py
+++ b/superset/mcp_service/constants.py
@@ -16,6 +16,10 @@
 # under the License.
 """Constants for the MCP service."""
 
+# Pagination defaults
+DEFAULT_PAGE_SIZE = 10  # Default number of items per page
+MAX_PAGE_SIZE = 100  # Maximum allowed page_size to prevent oversized responses
+
 # Response size guard defaults
 DEFAULT_TOKEN_LIMIT = 25_000  # ~25k tokens prevents overwhelming LLM context windows
 DEFAULT_WARN_THRESHOLD_PCT = 80  # Log warnings above 80% of limit
diff --git a/superset/mcp_service/dashboard/schemas.py b/superset/mcp_service/dashboard/schemas.py
index 97661d1c794..99d42b2fa35 100644
--- a/superset/mcp_service/dashboard/schemas.py
+++ b/superset/mcp_service/dashboard/schemas.py
@@ -84,6 +84,7 @@ if TYPE_CHECKING:
 from superset.daos.base import ColumnOperator, ColumnOperatorEnum
 from superset.mcp_service.chart.schemas import ChartInfo, serialize_chart_object
 from superset.mcp_service.common.cache_schemas import MetadataCacheControl
+from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
 from superset.mcp_service.system.schemas import (
     PaginationInfo,
     RoleInfo,
@@ -244,7 +245,13 @@ class ListDashboardsRequest(MetadataCacheControl):
         Field(default=1, description="Page number for pagination (1-based)"),
     ]
     page_size: Annotated[
-        PositiveInt, Field(default=10, description="Number of items per page")
+        int,
+        Field(
+            default=DEFAULT_PAGE_SIZE,
+            gt=0,
+            le=MAX_PAGE_SIZE,
+            description=f"Number of items per page (max {MAX_PAGE_SIZE})",
+        ),
     ]
 
     @model_validator(mode="after")
diff --git a/superset/mcp_service/dataset/schemas.py b/superset/mcp_service/dataset/schemas.py
index 1fc5d67e112..2d88b8bbc38 100644
--- a/superset/mcp_service/dataset/schemas.py
+++ b/superset/mcp_service/dataset/schemas.py
@@ -35,6 +35,7 @@ from pydantic import (
 
 from superset.daos.base import ColumnOperator, ColumnOperatorEnum
 from superset.mcp_service.common.cache_schemas import MetadataCacheControl
+from superset.mcp_service.constants import DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE
 from superset.mcp_service.system.schemas import (
     PaginationInfo,
     serialize_user_object,
@@ -247,7 +248,13 @@ class ListDatasetsRequest(MetadataCacheControl):
         Field(default=1, description="Page number for pagination (1-based)"),
     ]
     page_size: Annotated[
-        PositiveInt, Field(default=10, description="Number of items per page")
+        int,
+        Field(
+            default=DEFAULT_PAGE_SIZE,
+            gt=0,
+            le=MAX_PAGE_SIZE,
+            description=f"Number of items per page (max {MAX_PAGE_SIZE})",
+        ),
     ]
 
     @model_validator(mode="after")
diff --git a/superset/mcp_service/mcp_core.py b/superset/mcp_service/mcp_core.py
index 0b1a817d26d..051aa20809a 100644
--- a/superset/mcp_service/mcp_core.py
+++ b/superset/mcp_service/mcp_core.py
@@ -142,6 +142,11 @@ class ModelListCore(BaseCore, Generic[L]):
         page: int = 0,
         page_size: int = 10,
     ) -> L:
+        from superset.mcp_service.constants import MAX_PAGE_SIZE
+
+        # Clamp page_size to MAX_PAGE_SIZE as defense-in-depth
+        page_size = min(page_size, MAX_PAGE_SIZE)
+
         # Parse filters using generic utility (accepts JSON string or object)
         from superset.mcp_service.utils.schema_utils import (
             parse_json_or_list,
diff --git a/tests/unit_tests/mcp_service/chart/tool/test_list_charts.py b/tests/unit_tests/mcp_service/chart/tool/test_list_charts.py
index c7fa505ac93..c5ef6469ecc 100644
--- a/tests/unit_tests/mcp_service/chart/tool/test_list_charts.py
+++ b/tests/unit_tests/mcp_service/chart/tool/test_list_charts.py
@@ -28,6 +28,7 @@ from superset.mcp_service.chart.schemas import (
     ChartFilter,
     ListChartsRequest,
 )
+from superset.mcp_service.constants import MAX_PAGE_SIZE
 
 
 @pytest.fixture
@@ -133,6 +134,19 @@ class TestListChartsRequestSchema:
         with pytest.raises(ValueError, match="Input should be greater than 0"):
             ListChartsRequest(page_size=0)
 
+    def test_page_size_exceeds_max(self):
+        """Test that page_size over MAX_PAGE_SIZE raises validation error."""
+        with pytest.raises(
+            ValueError,
+            match=f"Input should be less than or equal to {MAX_PAGE_SIZE}",
+        ):
+            ListChartsRequest(page_size=MAX_PAGE_SIZE + 1)
+
+    def test_page_size_at_max(self):
+        """Test that page_size at MAX_PAGE_SIZE is accepted."""
+        request = ListChartsRequest(page_size=MAX_PAGE_SIZE)
+        assert request.page_size == MAX_PAGE_SIZE
+
     def test_filter_validation(self):
         """Test that filter validation works correctly."""
         # Valid filter