You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ga...@apache.org on 2026/04/16 21:53:17 UTC

(superset) branch master updated: fix(mcp): replace inputSchema with parameters_hint in search_tools results by default (#39411)

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

gabotruiz 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 e5b3a9c25db fix(mcp): replace inputSchema with parameters_hint in search_tools results by default (#39411)
e5b3a9c25db is described below

commit e5b3a9c25db6b74f0510d07804a9dbc0f7bba40f
Author: Gabriel Torres Ruiz <ga...@gmail.com>
AuthorDate: Thu Apr 16 18:53:10 2026 -0300

    fix(mcp): replace inputSchema with parameters_hint in search_tools results by default (#39411)
---
 superset/mcp_service/mcp_config.py                 |  15 +-
 superset/mcp_service/server.py                     |  78 ++++++--
 .../mcp_service/test_tool_search_transform.py      | 220 ++++++++++++++++++++-
 3 files changed, 294 insertions(+), 19 deletions(-)

diff --git a/superset/mcp_service/mcp_config.py b/superset/mcp_service/mcp_config.py
index caa1d1fc2d0..e052e373e39 100644
--- a/superset/mcp_service/mcp_config.py
+++ b/superset/mcp_service/mcp_config.py
@@ -261,6 +261,18 @@ MCP_RESPONSE_SIZE_CONFIG: Dict[str, Any] = {
 # - Set compact_schemas=False to disable schema compaction only (full $defs
 #   and descriptions in search results, tool search still active).
 # - Set max_description_length=0 to disable description truncation only.
+#
+# Summary Mode (include_schemas):
+# --------------------------------
+# When include_schemas=False (default), search results omit inputSchema
+# entirely and include a lightweight "parameters_hint" field listing
+# top-level parameter names (e.g. "page, page_size, search, filters").
+# This reduces per-search token cost by ~80% vs compact mode while still
+# conveying what parameters a tool accepts.  Full schemas remain available
+# when invoking the tool via call_tool.
+# - Set include_schemas=True to restore full inputSchema in search results.
+# - compact_schemas is ignored when include_schemas=False (no schema to
+#   compact); max_description_length still applies in summary mode.
 # =============================================================================
 MCP_TOOL_SEARCH_CONFIG: Dict[str, Any] = {
     "enabled": True,  # Enabled by default — reduces initial context by ~70%
@@ -272,8 +284,9 @@ MCP_TOOL_SEARCH_CONFIG: Dict[str, Any] = {
     ],
     "search_tool_name": "search_tools",  # Name of the search tool
     "call_tool_name": "call_tool",  # Name of the call proxy tool
-    "compact_schemas": True,  # Strip $defs and simplify $ref in search results
+    "compact_schemas": True,  # Strip $defs/$ref (requires include_schemas=True)
     "max_description_length": 300,  # Truncate tool descriptions (0 = no truncation)
+    "include_schemas": False,  # False=summary mode (name+hint), True=full inputSchema
 }
 
 
diff --git a/superset/mcp_service/server.py b/superset/mcp_service/server.py
index 074914de4e7..735f75c5df8 100644
--- a/superset/mcp_service/server.py
+++ b/superset/mcp_service/server.py
@@ -254,6 +254,20 @@ def _truncate_description(text: str, max_length: int) -> str:
     return truncated.rstrip() + "..."
 
 
+def _extract_parameter_names(input_schema: dict[str, Any]) -> str:
+    """Extract top-level parameter names from a JSON Schema as a hint string.
+
+    Returns a comma-separated string of property names from the schema's
+    ``properties`` key, or an empty string if none are found.
+
+    Example: ``"page, page_size, search, filters, select_columns"``
+    """
+    properties = input_schema.get("properties", {})
+    if not properties:
+        return ""
+    return ", ".join(properties.keys())
+
+
 def _serialize_tools_without_output_schema(
     tools: Sequence[Any],
 ) -> list[dict[str, Any]]:
@@ -265,7 +279,9 @@ def _serialize_tools_without_output_schema(
     """
     results = []
     for tool in tools:
-        data = tool.to_mcp_tool().model_dump(mode="json", exclude_none=True)
+        data = tool.to_mcp_tool().model_dump(
+            mode="json", exclude_none=True, exclude={"outputSchema"}
+        )
         data.pop("outputSchema", None)
         if input_schema := data.get("inputSchema"):
             data["inputSchema"] = _strip_titles(input_schema)
@@ -273,22 +289,62 @@ def _serialize_tools_without_output_schema(
     return results
 
 
+def _build_summary_serializer(max_desc: int) -> Any:
+    """Build a summary-mode serializer that omits ``inputSchema``.
+
+    Returns a callable that serializes each tool to ``name``,
+    ``description`` (optionally truncated), and a ``parameters_hint``
+    string listing top-level parameter names.  ``inputSchema`` and
+    ``outputSchema`` are stripped entirely.
+    """
+
+    def _summary_serializer(tools: Sequence[Any]) -> list[dict[str, Any]]:
+        results = []
+        for tool in tools:
+            data = tool.to_mcp_tool().model_dump(
+                mode="json", exclude_none=True, exclude={"outputSchema"}
+            )
+            data.pop("outputSchema", None)
+            if input_schema := data.pop("inputSchema", None):
+                hint = _extract_parameter_names(input_schema)
+                if hint:
+                    data["parameters_hint"] = hint
+            if max_desc and (desc := data.get("description")):
+                data["description"] = _truncate_description(desc, max_desc)
+            results.append(data)
+        return results
+
+    return _summary_serializer
+
+
 def _create_search_result_serializer(
     config: dict[str, Any],
 ) -> Any:
     """Build a search-result serializer from the tool-search config.
 
-    When ``compact_schemas`` is enabled (default), the serializer applies
-    additional compaction on top of the base serialization:
+    When ``include_schemas`` is False (default), delegates to
+    :func:`_build_summary_serializer`, which strips ``inputSchema``
+    entirely and adds a ``parameters_hint`` field with comma-separated
+    top-level parameter names.  This reduces per-search token cost by
+    ~80% vs compact mode while still conveying what parameters a tool
+    accepts.
 
-    * ``$defs`` sections and ``$ref`` pointers are collapsed
-      (see :func:`_compact_schema`).
+    When ``include_schemas`` is True, the full ``compact_schemas``/
+    ``max_description_length`` pipeline applies (existing behavior):
+
+    * ``$defs`` sections and ``$ref`` pointers are collapsed when
+      ``compact_schemas`` is True (see :func:`_compact_schema`).
     * Tool descriptions are truncated to ``max_description_length`` chars.
 
-    This reduces per-search-call token cost by ~40-60 % while keeping
-    enough detail for the LLM to identify the right tool and construct
-    a basic invocation.
+    Full schemas remain available when the tool is invoked via ``call_tool``.
     """
+    include_schemas = config.get("include_schemas", False)
+
+    if not include_schemas:
+        max_desc = config.get("max_description_length", 300)
+        return _build_summary_serializer(max_desc)
+
+    # include_schemas=True: apply full compact_schemas/max_description_length pipeline
     compact = config.get("compact_schemas", True)
     # Description truncation defaults to 300 when compact_schemas is on,
     # but is disabled when compact_schemas is off (unless explicitly set).
@@ -304,10 +360,8 @@ def _create_search_result_serializer(
             if compact:
                 if input_schema := data.get("inputSchema"):
                     data["inputSchema"] = _compact_schema(input_schema)
-            if max_desc and "description" in data:
-                data["description"] = _truncate_description(
-                    data["description"], max_desc
-                )
+            if max_desc and (desc := data.get("description")):
+                data["description"] = _truncate_description(desc, max_desc)
         return results
 
     return _serializer
diff --git a/tests/unit_tests/mcp_service/test_tool_search_transform.py b/tests/unit_tests/mcp_service/test_tool_search_transform.py
index dff91c76f81..74de17b88e5 100644
--- a/tests/unit_tests/mcp_service/test_tool_search_transform.py
+++ b/tests/unit_tests/mcp_service/test_tool_search_transform.py
@@ -27,6 +27,7 @@ from superset.mcp_service.server import (
     _apply_tool_search_transform,
     _compact_schema,
     _create_search_result_serializer,
+    _extract_parameter_names,
     _fix_call_tool_arguments,
     _normalize_call_tool_arguments,
     _serialize_tools_without_output_schema,
@@ -44,6 +45,7 @@ def test_tool_search_config_defaults():
     assert "get_instance_info" in MCP_TOOL_SEARCH_CONFIG["always_visible"]
     assert MCP_TOOL_SEARCH_CONFIG["search_tool_name"] == "search_tools"
     assert MCP_TOOL_SEARCH_CONFIG["call_tool_name"] == "call_tool"
+    assert MCP_TOOL_SEARCH_CONFIG["include_schemas"] is False
 
 
 def test_apply_bm25_transform():
@@ -514,7 +516,9 @@ def test_create_serializer_compacts_schemas():
         },
     )
 
-    serializer = _create_search_result_serializer({"compact_schemas": True})
+    serializer = _create_search_result_serializer(
+        {"include_schemas": True, "compact_schemas": True}
+    )
     result = serializer([tool])
 
     assert len(result) == 1
@@ -550,7 +554,7 @@ def test_create_serializer_disabled():
     )
 
     serializer = _create_search_result_serializer(
-        {"compact_schemas": False, "max_description_length": 0}
+        {"include_schemas": True, "compact_schemas": False, "max_description_length": 0}
     )
     result = serializer([tool])
 
@@ -569,7 +573,9 @@ def test_create_serializer_compact_false_disables_truncation():
         {"type": "object", "$defs": {"Model": {"type": "object"}}},
     )
 
-    serializer = _create_search_result_serializer({"compact_schemas": False})
+    serializer = _create_search_result_serializer(
+        {"include_schemas": True, "compact_schemas": False}
+    )
     result = serializer([tool])
 
     # $defs should still be present (compaction disabled)
@@ -588,7 +594,11 @@ def test_create_serializer_compact_false_explicit_truncation():
     )
 
     serializer = _create_search_result_serializer(
-        {"compact_schemas": False, "max_description_length": 200}
+        {
+            "include_schemas": True,
+            "compact_schemas": False,
+            "max_description_length": 200,
+        }
     )
     result = serializer([tool])
 
@@ -599,7 +609,11 @@ def test_create_serializer_compact_false_explicit_truncation():
 
 
 def test_create_serializer_uses_config_defaults():
-    """Empty config uses defaults (compact=True, max_desc=300)."""
+    """Empty config defaults to summary mode (include_schemas=False).
+
+    The new default omits inputSchema and adds parameters_hint instead.
+    Descriptions are still truncated to 300 chars.
+    """
     long_desc = "First sentence. " + "x" * 500
     tool = _make_mock_tool(
         "test_tool",
@@ -614,7 +628,10 @@ def test_create_serializer_uses_config_defaults():
     serializer = _create_search_result_serializer({})
     result = serializer([tool])
 
-    assert "$defs" not in result[0]["inputSchema"]
+    # Summary mode: no inputSchema, parameters_hint present
+    assert "inputSchema" not in result[0]
+    assert result[0]["parameters_hint"] == "x"
+    # Description still truncated to default 300
     assert len(result[0]["description"]) <= 303
 
 
@@ -640,3 +657,194 @@ def test_apply_transform_uses_compact_serializer():
         transform._search_result_serializer
         is not _serialize_tools_without_output_schema
     )
+
+
+# -- _extract_parameter_names tests --
+
+
+def test_extract_parameter_names_basic():
+    """Returns comma-separated top-level property names."""
+    schema = {
+        "type": "object",
+        "properties": {
+            "page": {"type": "integer"},
+            "page_size": {"type": "integer"},
+            "search": {"type": "string"},
+        },
+    }
+
+    result = _extract_parameter_names(schema)
+
+    assert result == "page, page_size, search"
+
+
+def test_extract_parameter_names_empty_properties():
+    """Returns empty string when properties dict is empty."""
+    schema = {"type": "object", "properties": {}}
+
+    result = _extract_parameter_names(schema)
+
+    assert result == ""
+
+
+def test_extract_parameter_names_no_properties_key():
+    """Returns empty string when properties key is absent."""
+    schema = {"type": "object"}
+
+    result = _extract_parameter_names(schema)
+
+    assert result == ""
+
+
+def test_extract_parameter_names_with_refs():
+    """Extracts names regardless of the shape of property values."""
+    schema = {
+        "type": "object",
+        "properties": {
+            "filters": {"type": "array", "items": {"$ref": "#/$defs/ChartFilter"}},
+            "select_columns": {"type": "array"},
+        },
+        "$defs": {"ChartFilter": {"type": "object"}},
+    }
+
+    result = _extract_parameter_names(schema)
+
+    assert result == "filters, select_columns"
+
+
+# -- _create_search_result_serializer summary mode (include_schemas=False) --
+
+
+def test_create_serializer_summary_mode_strips_input_schema():
+    """When include_schemas=False, inputSchema is absent from results."""
+    tool = _make_mock_tool(
+        "list_charts",
+        "List charts.",
+        {
+            "type": "object",
+            "properties": {
+                "page": {"type": "integer"},
+                "search": {"type": "string"},
+            },
+        },
+    )
+
+    serializer = _create_search_result_serializer({"include_schemas": False})
+    result = serializer([tool])
+
+    assert len(result) == 1
+    assert "inputSchema" not in result[0]
+    assert result[0]["name"] == "list_charts"
+
+
+def test_create_serializer_summary_mode_adds_parameters_hint():
+    """When include_schemas=False, parameters_hint lists top-level param names."""
+    tool = _make_mock_tool(
+        "list_charts",
+        "List charts.",
+        {
+            "type": "object",
+            "properties": {
+                "page": {"type": "integer"},
+                "page_size": {"type": "integer"},
+                "search": {"type": "string"},
+            },
+        },
+    )
+
+    serializer = _create_search_result_serializer({"include_schemas": False})
+    result = serializer([tool])
+
+    assert result[0]["parameters_hint"] == "page, page_size, search"
+
+
+def test_create_serializer_summary_mode_no_hint_when_no_properties():
+    """When inputSchema has no properties, parameters_hint is absent."""
+    tool = _make_mock_tool(
+        "health_check",
+        "Health check.",
+        {"type": "object"},
+    )
+
+    serializer = _create_search_result_serializer({"include_schemas": False})
+    result = serializer([tool])
+
+    assert "inputSchema" not in result[0]
+    assert "parameters_hint" not in result[0]
+
+
+def test_create_serializer_summary_mode_truncates_description():
+    """Summary mode still truncates descriptions to max_description_length."""
+    long_desc = "First sentence. " + "x" * 500
+    tool = _make_mock_tool(
+        "list_charts",
+        long_desc,
+        {"type": "object", "properties": {"page": {"type": "integer"}}},
+    )
+
+    serializer = _create_search_result_serializer(
+        {"include_schemas": False, "max_description_length": 50}
+    )
+    result = serializer([tool])
+
+    assert len(result[0]["description"]) <= 53
+
+
+def test_create_serializer_summary_mode_is_default():
+    """Empty config defaults to summary mode (include_schemas=False)."""
+    tool = _make_mock_tool(
+        "list_charts",
+        "List charts.",
+        {
+            "type": "object",
+            "properties": {"page": {"type": "integer"}},
+        },
+    )
+
+    serializer = _create_search_result_serializer({})
+    result = serializer([tool])
+
+    assert "inputSchema" not in result[0]
+    assert "parameters_hint" in result[0]
+
+
+def test_create_serializer_include_schemas_true_restores_full_schema():
+    """include_schemas=True preserves inputSchema in results."""
+    schema = {
+        "type": "object",
+        "properties": {"page": {"type": "integer"}},
+        "$defs": {"Model": {"type": "object"}},
+    }
+    tool = _make_mock_tool("list_charts", "List charts.", schema)
+
+    serializer = _create_search_result_serializer(
+        {"include_schemas": True, "compact_schemas": False, "max_description_length": 0}
+    )
+    result = serializer([tool])
+
+    assert "inputSchema" in result[0]
+    assert "parameters_hint" not in result[0]
+    assert "$defs" in result[0]["inputSchema"]
+
+
+def test_create_serializer_include_schemas_true_with_compact():
+    """include_schemas=True + compact_schemas=True still compacts the schema."""
+    schema = {
+        "type": "object",
+        "properties": {
+            "filters": {"type": "array", "items": {"$ref": "#/$defs/ChartFilter"}}
+        },
+        "$defs": {"ChartFilter": {"type": "object"}},
+    }
+    tool = _make_mock_tool("list_charts", "List charts.", schema)
+
+    serializer = _create_search_result_serializer(
+        {"include_schemas": True, "compact_schemas": True}
+    )
+    result = serializer([tool])
+
+    assert "inputSchema" in result[0]
+    assert "$defs" not in result[0]["inputSchema"]
+    assert result[0]["inputSchema"]["properties"]["filters"]["items"] == {
+        "type": "object"
+    }