aminghadersohi commented on code in PR #44656:
URL: https://github.com/apache/superset/pull/44656#discussion_r4161436722


##########
superset/mcp_service/server.py:
##########
@@ -301,29 +302,29 @@ def _strip_titles(obj: Any, in_properties_map: bool = 
False) -> Any:
 
 
 def _truncate_description(text: str, max_length: int) -> str:
-    """Truncate a tool description for search results.
-
-    Cuts at the last sentence boundary before *max_length*, or at
-    *max_length* with an ellipsis if no sentence boundary is found.
-
-    Dedents first: Python 3.13 has the compiler strip a docstring's common
-    leading whitespace at compile time (``__doc__`` comes out already
-    cleaned), while 3.11/3.12 store it raw and leave that to the caller. A
-    multi-line tool docstring's raw, un-dedented form is longer per line, so
-    the same character budget lands at a different point in the text
-    depending on which Python compiled it. Cleaning here first makes the cut
-    point (and this function's callers' byte budgets) consistent regardless
-    of interpreter version.
+    """Keep whole paragraphs, or sentences in single-paragraph prose, within 
budget.
+
+    Clean docstring indentation before applying the budget so the cut point
+    is consistent across Python versions that store docstrings differently.
     """
+    if max_length <= 0:
+        return ""
     text = inspect.cleandoc(text) if text else text
     if not text or len(text) <= max_length:
         return text
-    # Try to cut at the last sentence boundary
-    truncated = text[:max_length]
-    last_period = truncated.rfind(". ")
-    if last_period > max_length // 2:
-        return truncated[: last_period + 1]
-    return truncated.rstrip() + "..."
+    # Do not leave a heading or a numbered/bulleted workflow partly advertised.
+    if paragraphs := list(re.finditer(r"\n\s*\n", text)):
+        ends = [match.start() for match in paragraphs if match.start() <= 
max_length]
+        return text[: ends[-1]].strip() if ends else ""
+    # Calling constraints belong in request schema metadata, not truncated 
prose.
+    boundaries = list(re.finditer(r"[.!?](?=\s|$)", text[: max_length + 1]))
+    ends = [match.end() for match in boundaries if match.end() <= max_length]
+    return text[: ends[-1]].strip() if ends else ""
+
+
+def _request_instructions(input_schema: dict[str, Any]) -> str:
+    """Read unabridged calling instructions from the request wrapper's 
schema."""
+    return input_schema.get("properties", {}).get("request", 
{}).get("description", "")

Review Comment:
   Fixed in 81bcc73ef3 (branch head 78e8a55a06). `_request_instructions` now 
takes the tool and returns only the `Field(description=...)` text on its 
`request` parameter. The request model's docstring, which the list_tools 
middleware inlines onto `properties.request`, is no longer deducted from the 
budget or appended to `parameters_hint`. Untouched tools' hints are back to 
bare parameter names.
   
   Served `search_tools` entries (`list_tools(run_middleware=True)`, default 
config, 300 limit) compared with current master `33292f249d`:
   
   | | master | 87a5768 | 78e8a55 |
   | --- | --- | --- | --- |
   | empty `description` | 0 | 9 | 0 |
   | `manage_dashboard_owners` | 303 | 0 | 128 |
   | `find_users` | 186 | 0 | 186 |
   | `generate_bug_report` | 158 | 0 | 158 |
   | `get_chart_sql` | 161 | 0 | 256 |
   | summary-mode bytes | 42,041 | 38,679 | 39,761 |
   | schema-mode bytes | 162,527 | 152,790 | 160,080 |
   
   `manage_dashboard_owners` stops at 128 because master's extra text came from 
the old hard cut ("...and rejects any cha..."); the next complete sentence does 
not fit in 300. The ten touched tools are unchanged and still pass every 
constraint test.
   
   New regression tests: 
`test_served_discovery_keeps_untouched_tool_descriptions` checks every served 
tool in both modes. Each must have a non-empty description; tools outside the 
ten must have no deduction and a parameter-name-only hint. 
`test_served_discovery_regressions_keep_prose` covers your nine tools plus 
`delete_chart`. Both fail at 87a5768 and pass at 78e8a55.
   



##########
superset/mcp_service/server.py:
##########
@@ -301,29 +302,29 @@ def _strip_titles(obj: Any, in_properties_map: bool = 
False) -> Any:
 
 
 def _truncate_description(text: str, max_length: int) -> str:
-    """Truncate a tool description for search results.
-
-    Cuts at the last sentence boundary before *max_length*, or at
-    *max_length* with an ellipsis if no sentence boundary is found.
-
-    Dedents first: Python 3.13 has the compiler strip a docstring's common
-    leading whitespace at compile time (``__doc__`` comes out already
-    cleaned), while 3.11/3.12 store it raw and leave that to the caller. A
-    multi-line tool docstring's raw, un-dedented form is longer per line, so
-    the same character budget lands at a different point in the text
-    depending on which Python compiled it. Cleaning here first makes the cut
-    point (and this function's callers' byte budgets) consistent regardless
-    of interpreter version.
+    """Keep whole paragraphs, or sentences in single-paragraph prose, within 
budget.
+
+    Clean docstring indentation before applying the budget so the cut point
+    is consistent across Python versions that store docstrings differently.
     """
+    if max_length <= 0:
+        return ""
     text = inspect.cleandoc(text) if text else text
     if not text or len(text) <= max_length:
         return text
-    # Try to cut at the last sentence boundary
-    truncated = text[:max_length]
-    last_period = truncated.rfind(". ")
-    if last_period > max_length // 2:
-        return truncated[: last_period + 1]
-    return truncated.rstrip() + "..."
+    # Do not leave a heading or a numbered/bulleted workflow partly advertised.
+    if paragraphs := list(re.finditer(r"\n\s*\n", text)):
+        ends = [match.start() for match in paragraphs if match.start() <= 
max_length]
+        return text[: ends[-1]].strip() if ends else ""

Review Comment:
   Folded in with 81bcc73ef3 (branch head 78e8a55a06). After the last whole 
paragraph that fits, `_truncate_description` adds the complete sentences of the 
next paragraph that fit. It skips a next paragraph that has list items (your 
guard), opens with `IMPORTANT`, or has a first line ending in `:` (`Workflow:`, 
`IMPORTANT BEHAVIOR:`), so partial workflows and blocks stay excluded. An 
overlong first paragraph now falls back to its leading sentences instead of 
`""`.
   
   Served entries at the default 300, compared with current master 
`33292f249d`: untouched tools shorter than master dropped from 49 (8,832 chars) 
to 25 (2,555 chars). `delete_chart` is back to the full 263 characters, 
including "NOT chart name". Of the remaining 25, 24 match or exceed master's 
text after its trailing hard-cut fragment ("...") is trimmed back to whole 
sentences. The last is `list_metrics` (279 vs 293), where master's extra text 
is the "Workflow:\n1." fragment. `get_schema` correctly stays on its summary 
line.
   
   Unit tests in `test_tool_search_transform.py` cover the sentence fallback, 
the list/heading guards, and the long first paragraph.
   



##########
tests/unit_tests/mcp_service/test_tool_description_constraints.py:
##########
@@ -0,0 +1,591 @@
+# 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.
+
+"""Calling guidance must survive discovery without relying on truncated 
prose."""
+
+import inspect
+import re
+
+import pytest
+
+from superset.mcp_service.app import mcp
+from superset.mcp_service.server import _create_search_result_serializer
+
+CONSTRAINTS = {
+    "list_dashboards": ("Do NOT pass", "top-level", '{"request": {'),
+    "list_charts": ("Do NOT pass", "top-level", '{"request": {'),
+    "list_datasets": (
+        "Do NOT pass",
+        "search/page/filters",
+        "top-level",
+        "out-of-scope",
+        '{"request": {',
+    ),
+    "get_chart_info": (
+        "NOT chart name",
+        "form_data_key",
+        "permalink_key",
+        '{"request": {',
+    ),
+    "get_dashboard_info": (
+        "filter_state",
+        "permalink_key",
+        "snapshots, not query predicates",
+        '{"request": {',
+    ),
+    "generate_dashboard": ("Never use as a fallback", "accessible", 
'{"request": {'),
+}
+
+
[email protected]
[email protected]("name", CONSTRAINTS)
[email protected]("include_schemas", [True, False])
[email protected]("max_desc", [1, 20, 300, 0])
+async def test_registered_tool_calling_constraints(
+    name: str, include_schemas: bool, max_desc: int
+) -> None:
+    """Exercise real registrations, including small limits and summary 
discovery."""
+    tool = await mcp.get_tool(name)
+    assert tool is not None
+    entry = _create_search_result_serializer(
+        {"include_schemas": include_schemas, "max_description_length": 
max_desc}
+    )([tool])[0]
+    if include_schemas:
+        guidance = 
entry["inputSchema"]["properties"]["request"].get("description", "")
+        schema_text = _schema_text(entry["inputSchema"])
+        for _, phrases in SCHEMA_DOCSTRING_CONSTRAINTS[name]:
+            assert all(phrase in schema_text for phrase in phrases), (name, 
phrases)
+    else:
+        guidance = str(entry.get("parameters_hint", ""))
+    for constraint in CONSTRAINTS[name]:
+        assert constraint in guidance, (name, constraint, entry["description"])
+    if max_desc:
+        assert len(entry["description"]) <= max_desc
+
+
[email protected]
[email protected]("name", CONSTRAINTS)
+async def test_registered_constraints_have_bounded_size(name: str) -> None:
+    """Critical metadata has its own fixed ceiling, not an unbounded IMPORTANT 
block."""
+    tool = await mcp.get_tool(name)
+    assert tool is not None
+    instructions = tool.parameters["properties"]["request"]["description"]
+    assert len(instructions) <= 300
+    entry = _create_search_result_serializer({"include_schemas": 
True})([tool])[0]
+    assert len(instructions) + len(entry["description"]) <= 300
+
+
[email protected]
[email protected]("name", CONSTRAINTS)
[email protected]("include_schemas", [True, False])
+async def test_oversized_registered_description_keeps_calling_constraints(
+    name: str, include_schemas: bool
+) -> None:
+    """Long introductory prose cannot displace metadata or exhaust its 
budget."""
+    tool = await mcp.get_tool(name)
+    assert tool is not None
+    oversized = tool.model_copy(
+        update={"description": "Long introduction. " * 10_000 + 
tool.description}
+    )
+    entry = _create_search_result_serializer({"include_schemas": 
include_schemas})(
+        [oversized]
+    )[0]
+    instructions = tool.parameters["properties"]["request"]["description"]
+    if include_schemas:
+        assert entry["inputSchema"]["properties"]["request"]["description"] == 
(
+            instructions
+        )
+        schema_text = _schema_text(entry["inputSchema"])
+        for _, phrases in SCHEMA_DOCSTRING_CONSTRAINTS[name]:
+            assert all(phrase in schema_text for phrase in phrases), (name, 
phrases)
+    else:
+        assert instructions in entry["parameters_hint"]
+    assert len(entry["description"]) + len(instructions) <= 300
+
+
[email protected]
+async def test_direct_inventory_keeps_bounded_calling_metadata() -> None:
+    """Direct tools/list also carries instructions without relying on 
search."""
+    for tool in await mcp.list_tools(run_middleware=False):

Review Comment:
   Done in 81bcc73ef3 (branch head 78e8a55a06). 
`test_direct_inventory_keeps_bounded_calling_metadata` now reads 
`mcp.list_tools(run_middleware=True)`. For every served tool, it asserts the 
instructions the serializer deducts and advertises are at most 300 characters. 
When instructions are present, they must equal the served 
`properties.request.description`. Model docstrings inlined by the middleware 
(up to 633 characters, e.g. `manage_dashboard_owners`) no longer count as 
instructions. The new `test_served_discovery_keeps_untouched_tool_descriptions` 
also asserts the 300 cap on every served `description` in both modes.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to