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"
+ }