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


##########
superset/mcp_service/chart/schemas.py:
##########
@@ -6396,16 +6416,24 @@ def _detect_chart_name_sanitization(cls, data: Any) -> 
Any:
 
     @field_validator("chart_name")
     @classmethod
-    def sanitize_chart_name(cls, v: str | None) -> str | None:
-        """Sanitize chart name to prevent XSS attacks."""
-        try:
-            return sanitize_user_input(
-                v, "Chart name", max_length=255, allow_empty=True
-            )
-        except ValueError as ex:
-            raise PydanticCustomError(
-                "unsafe_text", "{error}", {"error": str(ex)}
-            ) from ex
+    def normalize_blank_chart_name(cls, v: str | None) -> str | None:
+        """Keep the optional-name fallback; nonblank names are sanitized 
above."""
+        return v if v and v.strip() else None

Review Comment:
   The remaining namespace byte-input gap is fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852. `CreateDatasetRequest.schema_` 
(including the `schema` alias) and `catalog` decode UTF-8 bytes/bytearray 
before trimming, blank-to-None normalization, and the 250-character cap. 
Invalid UTF-8 chart/dashboard titles retain a field-located `string_unicode` 
error. The audit also found and corrected the same string-only bypass in 
`GetChartInfoRequest.permalink_key` URL extraction.
   
   Regression tests in 
tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py, run against 
untouched source at 746c57391cecb0fba4771a678914bfa1131efd73 and this commit:
   - test_byte_dataset_namespace_normalizes_before_length: 24 fail before / 
pass after; covers padded values, short/251-character blanks, exact-limit 
names, both byte types, both namespace fields, and the schema alias. It also 
checks the exact properties passed to dataset creation and omission of blank 
namespaces.
   - test_invalid_utf8_title_error_is_field_located: 48 fail before / pass 
after across all title-writing models and aliases, including saved/unsaved 
chart generation.
   - test_byte_permalink_url_extracts_key and 
test_byte_permalink_rejects_other_url: 4 fail before / pass after, covering the 
success and rejection paths.
   - test_dataset_namespace_rejects_malformed_values: 21 passing controls 
before and after, preserving type rejection, field locations, and the real 
over-limit cap.
   
   Final regression comparison: 76 failed / 21 passed before; 97 passed after. 
Full touched files: 1,633 passed. Changed-file pre-commit and explicit mypy 
passed; the branch's three frontend Jest suites passed (32 tests). Real-LLM 
checks excluded (OpenRouter disabled).
   
   I searched every `@field_validator` in `superset/mcp_service/**/schemas.py` 
with an AST inventory (88 validators), plus `rg -n 
'validator|isinstance.*str|decode\(' superset/mcp_service -g schemas.py` and 
callers of `CreateDatasetRequest`, `GetChartInfoRequest`, and 
`field_validation_error`. Full sibling audit follows; earlier already-decoding 
paths are distinguished from the namespace/title/permalink changes in this 
commit.
   
   Siblings checked:
   - superset/mcp_service/catalog/schemas.py:79 — not affected: 
GetCatalogRequest._normalize_search: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/catalog/schemas.py:87 — not affected: 
GetCatalogRequest._normalize_cursor: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:452 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/chart/schemas.py:484 — not affected: 
GetChartInfoRequest._parse_select_columns: list parser; bare bytes are rejected 
by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/chart/schemas.py:879 — not affected: 
BaseChartConfig.sanitize_temporal_column: already decodes bytes/bytearray 
before text normalization; field-validator errors already carry a field 
location.
   - superset/mcp_service/chart/schemas.py:1005 — not affected: 
ColumnRef.sanitize_name: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1017 — not affected: 
ColumnRef.sanitize_label: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1027 — not affected: 
ColumnRef.sanitize_sql: runs after Pydantic validates/coerces its declared 
field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:1140 — not affected: 
FilterConfig.sanitize_column: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1152 — not affected: 
FilterConfig.sanitize_value: runs after Pydantic validates/coerces its declared 
field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:1223 — not affected: 
GanttSortByConfig.sanitize_column: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1547 — not affected: 
GaugeChartConfig.validate_gauge_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:1712 — not affected: 
TreemapChartUpdateConfig.validate_treemap_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:2335 — not affected: 
SunburstLegacySavedFields.validate_legacy_limit: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:2777 — not affected: 
SunburstChartConfig.sanitize_time_range: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:3173 — not affected: 
InteractivePivotChartConfig.sanitize_comparison_period: already decodes 
bytes/bytearray before text normalization; field-validator errors already carry 
a field location.
   - superset/mcp_service/chart/schemas.py:3340 — not affected: 
MixedTimeseriesChartConfig.wrap_single_group_by: list/model shorthand; byte 
input cannot bypass into a stored string because the target list/model 
validation rejects it.
   - superset/mcp_service/chart/schemas.py:4080 — not affected: 
XYChartConfig.coerce_sort_by: list/model shorthand; byte input cannot bypass 
into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4105 — not affected: 
XYChartConfig.coerce_series_ranking_metric: list/model shorthand; byte input 
cannot bypass into a stored string because the target list/model validation 
rejects it.
   - superset/mcp_service/chart/schemas.py:4117 — not affected: 
XYChartConfig.validate_series_ranking_metric: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:4129 — not affected: 
XYChartConfig.wrap_single_group_by: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4134 — not affected: 
XYChartConfig.coerce_x_column_name: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4140 — not affected: 
XYChartConfig.coerce_y_column_names: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4156 — not affected: 
XYChartConfig.coerce_legend_flag: boolean-to-model shorthand, not a string-only 
normalizer.
   - superset/mcp_service/chart/schemas.py:4611 — not affected: 
BulletChartConfig.tokenize_native_numeric_lists: native list shorthand; bare 
bytes are rejected by the list field, and label list elements sanitize after 
string coercion.
   - superset/mcp_service/chart/schemas.py:4623 — not affected: 
BulletChartConfig.tokenize_native_label_lists: native list shorthand; bare 
bytes are rejected by the list field, and label list elements sanitize after 
string coercion.
   - superset/mcp_service/chart/schemas.py:4633 — not affected: 
BulletChartConfig.reject_non_finite_values: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:4640 — not affected: 
BulletChartConfig.validate_presentation_labels: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:4660 — not affected: 
BulletChartConfig.sanitize_time_range: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:5625 — not affected: 
GanttChartConfig.coerce_single_column: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:5631 — not affected: 
GanttChartConfig.coerce_tooltip_columns: list/model shorthand; byte input 
cannot bypass into a stored string because the target list/model validation 
rejects it.
   - superset/mcp_service/chart/schemas.py:5638 — not affected: 
GanttChartConfig.parse_native_order_by: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:5674 — not affected: 
GanttChartConfig.validate_gantt_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:5679 — not affected: 
GanttChartConfig.validate_time_bounds: runs after Pydantic validates/coerces 
its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:5805 — not affected: 
GeographicChartConfig.validate_geographic_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:6426 — not affected: 
ChartNameMixin.normalize_blank_chart_name: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:7297 — not affected: 
RestoreChartRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/chart/schemas.py:7339 — not affected: 
DeleteChartRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:371 — not affected: 
GetDashboardInfoRequest._parse_select_columns: list parser; bare bytes are 
rejected by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/dashboard/schemas.py:913 — not affected: 
GenerateDashboardRequest.normalize_blank_dashboard_title: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:1084 — not affected: 
UpdateDashboardRequest.normalize_blank_dashboard_title: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:1094 — not affected: 
UpdateDashboardRequest.normalize_slug: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/dashboard/schemas.py:1195 — not affected: 
ManageDashboardOwnersRequest.reject_bool_identifier: boolean-ID/numeric guard, 
not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1204 — not affected: 
ManageDashboardOwnersRequest.reject_bool_owner_ids: boolean-ID/numeric guard, 
not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1294 — not affected: 
ManageDashboardRolesRequest.reject_bool_identifier: boolean-ID/numeric guard, 
not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1303 — not affected: 
ManageDashboardRolesRequest.reject_bool_role_ids: boolean-ID/numeric guard, not 
a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1394 — not affected: 
ManageDashboardCertificationRequest.reject_bool_identifier: boolean-ID/numeric 
guard, not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1403 — not affected: 
ManageDashboardCertificationRequest.sanitize_certified_by: already decodes 
bytes/bytearray before text normalization; field-validator errors already carry 
a field location.
   - superset/mcp_service/dashboard/schemas.py:1427 — not affected: 
ManageDashboardCertificationRequest.sanitize_certification_details: already 
decodes bytes/bytearray before text normalization; field-validator errors 
already carry a field location.
   - superset/mcp_service/dashboard/schemas.py:1581 — not affected: 
DuplicateDashboardRequest.reject_blank_dashboard_title: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2315 — not affected: 
DeleteDashboardRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:2380 — not affected: 
BaseNewFilterSpec.reject_bool_dataset_id: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:2448 — not affected: 
FilterTimeSpec._validate_default_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2582 — not affected: 
NativeFilterUpdateSpec._validate_default_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2596 — not affected: 
NativeFilterUpdateSpec.reject_bool_dataset_id: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:2773 — not affected: 
HeaderComponentSpec.sanitize_text: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2834 — not affected: 
DashboardComponentUpdateSpec.sanitize_text: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:3005 — not affected: 
ApplyFilterValueSpec._validate_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:3011 — not affected: 
ApplyFilterValueSpec._validate_range_order: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:3483 — not affected: 
RestoreDashboardRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:3570 — not affected: 
GetDashboardDataRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:344 — not affected: 
ListDatasetsRequest.parse_filters: passthrough with no string-only 
normalization.
   - superset/mcp_service/dataset/schemas.py:349 — not affected: 
ListDatasetsRequest.parse_select_columns: passthrough with no string-only 
normalization.
   - superset/mcp_service/dataset/schemas.py:439 — not affected: 
GetDatasetInfoRequest._parse_select_columns: list parser; bare bytes are 
rejected by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/dataset/schemas.py:449 — not affected: 
GetDatasetInfoRequest._parse_column_fields: list parser; bare bytes are 
rejected by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/dataset/schemas.py:474 — not affected: 
CreateDatasetMetric.expression_must_aggregate: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:485 — not affected: 
CreateDatasetMetric.d3format_must_be_valid: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:556 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dataset/schemas.py:571 — not affected: 
CreateDatasetRequest._strip_table_name: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/dataset/schemas.py:627 — not affected: 
CreateVirtualDatasetRequest.sql_must_not_be_empty: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:634 — not affected: 
CreateVirtualDatasetRequest.dataset_name_must_not_be_empty: already decodes 
bytes/bytearray before text normalization; field-validator errors already carry 
a field location.
   - superset/mcp_service/dataset/schemas.py:749 — not affected: 
DatasetMetricProperties.d3format_must_be_valid: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:912 — not affected: 
DeleteDatasetRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:973 — not affected: 
RestoreDatasetRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:1056 — not affected: 
UpdateDatasetRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:1065 — not affected: 
UpdateDatasetRequest.reject_bool_cache_timeout: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:1257 — not affected: 
QueryDatasetRequest._validate_time_range: runs after Pydantic validates/coerces 
its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/semantic_layer/schemas.py:273 — not affected: 
GetTableRequest.normalize_time_grain: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/semantic_layer/schemas.py:309 — not affected: 
GetTableRequest._validate_time_range: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/sql_lab/schemas.py:108 — not affected: 
ExecuteSqlRequest.sql_not_empty: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/sql_lab/schemas.py:123 — not affected: 
ColumnInfo.coerce_is_nullable: response metadata coercion, not a request 
normalization path.
   - superset/mcp_service/sql_lab/schemas.py:263 — not affected: 
SaveSqlQueryRequest.sql_not_empty: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/sql_lab/schemas.py:270 — not affected: 
SaveSqlQueryRequest.label_not_empty: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/sql_lab/schemas.py:335 — not affected: 
OpenSqlLabRequest.title_strip_or_none: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/system/schemas.py:219 — not affected: 
FindUsersRequest._reject_blank_query: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/system/schemas.py:230 — not affected: 
FindUsersRequest._reject_email_shaped_query: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/theme/schemas.py:155 — not affected: 
CreateThemeRequest.reject_blank_theme_name: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/user/schemas.py:104 — not affected: 
UserInfo._extract_role_names: response metadata coercion, not a request 
normalization path.
   - superset/mcp_service/chart/schemas.py:6373 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dashboard/schemas.py:859 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dashboard/schemas.py:1031 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dashboard/schemas.py:1527 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/utils/schema_utils.py:41 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dataset/schemas.py:580 — not affected: 
CreateVirtualDatasetRequest schema/catalog do not promise trim/blank 
normalization for string inputs either.
   - superset/mcp_service/dataset/schemas.py:701 — not affected: 
DatasetMetricProperties and its create/update subclasses validate literal 
metric names after coercion; they deliberately retain whitespace.
   - superset/mcp_service/dataset/schemas.py:1012 — not affected: 
UpdateDatasetRequest validates its literal table_name after coercion; it 
deliberately retains whitespace.
   - superset/mcp_service/dataset/schemas.py:881 — not affected: slice_name is 
response metadata, not a title-writing request.
   - superset/mcp_service/chart/schemas.py:6469 — not affected: 
GenerateExploreLinkRequest has no chart-name field.
   - superset/mcp_service/chart/schemas.py:6576 — not affected: 
UpdateChartPreviewRequest has no chart-name field.
   - superset/mcp_service/dataset/tool/create_dataset.py:97 — not affected: 
_build_dataset_properties consumes the normalized typed request; the new 
namespace regression also asserts the exact persisted properties and omitted 
blank keys.
   - superset/mcp_service/chart/tool/get_chart_info.py:550 — not affected: the 
cache loader consumes the validated permalink key; it has no separate raw-input 
decoder.
   - superset/mcp_service/chart/tool/generate_chart.py:255 — not affected: 
revalidation carries the already-sanitized chart_name; no second raw-byte entry 
point.
   - superset/mcp_service/chart/tool/update_chart.py — not affected: consumes 
UpdateChartRequest, which inherits the shared chart-name guard.
   - superset/mcp_service/dashboard/tool/generate_dashboard.py — not affected: 
consumes GenerateDashboardRequest; shared schema owns byte decoding.
   - superset/mcp_service/dashboard/tool/update_dashboard.py — not affected: 
consumes UpdateDashboardRequest; shared schema owns byte decoding.
   - superset/mcp_service/dashboard/tool/duplicate_dashboard.py — not affected: 
consumes DuplicateDashboardRequest; shared schema owns byte decoding.
   - superset/mcp_service/utils/validation.py:251 — not affected: formats only 
schema-backed locations and static reasons, without msg, ctx, or input.
   



##########
tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py:
##########
@@ -524,3 +579,789 @@ def validate(request: BaseModel) -> str:
     )
     assert f"request.{field}: Contains disallowed content" in message
     assert "private-input" not in message
+
+
[email protected]
[email protected](
+    ("tool", "payload", "field", "limit"),
+    [
+        ("generate_dashboard", {"chart_ids": [1]}, "dashboard_title", 500),
+        ("update_dashboard", {"identifier": 1}, "dashboard_title", 500),
+        ("duplicate_dashboard", {"dashboard_id": 1}, "dashboard_title", 500),
+        (
+            "generate_chart",
+            {
+                "dataset_id": 1,
+                "config": {"chart_type": "table", "columns": [{"name": "a"}]},
+            },
+            "chart_name",
+            250,
+        ),
+        ("update_chart", {"identifier": 1}, "chart_name", 250),
+    ],
+)
[email protected]("alias", [None, "title", "name"])
[email protected]("length", [None, 600])
+async def test_title_length_wire_reason(
+    tool: str,
+    payload: dict[str, Any],
+    field: str,
+    limit: int,
+    alias: str | None,
+    length: int | None,
+) -> None:
+    """Length errors retain their reason through warning detection and MCP."""
+    rejected = "x" * (length or limit + 1)
+    async with Client(make_title_server()) as client:
+        result = await client.call_tool_mcp(
+            tool, {"request": {**payload, alias or field: rejected}}
+        )
+    assert result.isError is True
+    assert result.content == [
+        TextContent(
+            type="text",
+            text=f"Error: Validation error in {tool}: request.{field}: "
+            "String exceeds the allowed maximum length",
+        )
+    ]
+
+
[email protected]
[email protected]("field", ["dashboard_title", "title", "name"])
[email protected]("rejected", ["", " \t\n"])
+async def test_duplicate_dashboard_empty_title_wire_reason(
+    field: str,
+    rejected: str,
+) -> None:
+    """Blank required titles have a static, actionable reason on the wire."""
+    async with Client(make_title_server()) as client:
+        result = await client.call_tool_mcp(
+            "duplicate_dashboard", {"request": {"dashboard_id": 1, field: 
rejected}}
+        )
+    assert result.isError is True
+    assert result.content == [
+        TextContent(
+            type="text",
+            text="Error: Validation error in duplicate_dashboard: "
+            "request.dashboard_title: Text must not be empty",
+        )
+    ]
+
+
+_REQUIRED_NAME_CASES: list[tuple[str, dict[str, Any], str, list[str | None]]] 
= [
+    (
+        "create_virtual_dataset",
+        {"database_id": 1, "sql": "select 1"},
+        "dataset_name",
+        ["", " \t\n"],
+    ),
+    ("create_theme", {"json_data": {}}, "theme_name", ["", " \t\n"]),
+    (
+        "create_dataset_metric",
+        {"dataset_id": 1, "expression": "count(*)"},
+        "metric_name",
+        ["", " \t\n"],
+    ),
+    (
+        "update_dataset_metric",
+        {"dataset_id": 1, "metric": 1},
+        "metric_name",
+        ["", " \t\n", None],
+    ),
+    ("update_dataset", {"dataset_id": 1}, "table_name", ["", " \t\n", None]),
+    (
+        "save_sql_query",
+        {"database_id": 1, "sql": "select 1"},
+        "label",
+        ["", " \t\n"],
+    ),
+]
+
+
[email protected]
[email protected](
+    ("tool", "payload", "field", "rejected"),
+    [
+        (tool, payload, field, rejected)
+        for tool, payload, field, blanks in _REQUIRED_NAME_CASES
+        for rejected in blanks
+    ],
+)
+async def test_required_name_empty_wire_reason(
+    tool: str, payload: dict[str, Any], field: str, rejected: str | None
+) -> None:
+    """Blank object names report a safe reason at the declared field."""
+    async with Client(make_name_server()) as client:
+        result = await client.call_tool_mcp(
+            tool, {"request": {**payload, field: rejected}}
+        )
+    assert result.isError is True
+    assert result.content == [
+        TextContent(
+            type="text",
+            text=f"Error: Validation error in {tool}: request.{field}: "
+            "Text must not be empty",
+        )
+    ]
+
+
[email protected](
+    "model,payload",
+    [
+        (
+            GenerateChartRequest,
+            {
+                "dataset_id": 1,
+                "config": {"chart_type": "table", "columns": [{"name": "a"}]},
+            },
+        ),
+        (UpdateChartRequest, {"identifier": 1}),
+    ],
+)
[email protected]("length", [250, 251, 252, 253, 254, 255])
+def test_chart_name_model_column_boundary(
+    model: type[BaseModel], payload: dict[str, Any], length: int
+) -> None:
+    """Chart names must fit the persisted Slice name column."""
+    from superset.models.slice import Slice
+
+    limit = Slice.__table__.c.slice_name.type.length
+    assert limit == 250
+    if length <= limit:
+        assert (
+            model.model_validate({**payload, "chart_name": "x" * 
length}).chart_name
+            == "x" * length
+        )
+    else:
+        with pytest.raises(ValidationError) as exc:
+            model.model_validate({**payload, "chart_name": "x" * length})
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+        assert exc.value.errors()[0]["loc"] == ("chart_name",)
+
+
[email protected](
+    "model,payload",
+    [
+        (
+            GenerateChartRequest,
+            {
+                "dataset_id": 1,
+                "config": {"chart_type": "table", "columns": [{"name": "a"}]},
+            },
+        ),
+        (UpdateChartRequest, {"identifier": 1}),
+    ],
+)
[email protected]("alias", ["chart_name", "name", "title", 
"slice_name"])
[email protected]("length", [250, 251])
+def test_padded_chart_name_boundary(
+    model: type[BaseModel], payload: dict[str, Any], alias: str, length: int
+) -> None:
+    """Validate the trimmed chart name against the model column boundary."""
+    value = "  " + "x" * length + "  "
+    if length == 250:
+        assert (
+            model.model_validate({**payload, alias: value}).chart_name == "x" 
* length
+        )
+    else:
+        with pytest.raises(ValidationError) as exc:
+            model.model_validate({**payload, alias: value})
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+
+
[email protected]("length", [250, 251, 310])
+def test_theme_name_model_column_boundary(length: int) -> None:
+    """Theme creation must respect its persisted name column."""
+    from superset.models.core import Theme
+
+    assert Theme.__table__.c.theme_name.type.length == 250
+    if length == 250:
+        assert (
+            CreateThemeRequest(theme_name="x" * length, 
json_data={}).theme_name
+            == "x" * length
+        )
+    else:
+        with pytest.raises(ValidationError) as exc:
+            CreateThemeRequest(theme_name="x" * length, json_data={})
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+
+
[email protected]
[email protected]("alias", ["dashboard_title", "title", "name"])
[email protected]("value", ["", " \t\n"])
+async def test_update_dashboard_blank_title_wire_reason(alias: str, value: 
str) -> None:
+    """Reject blank renames rather than reporting a title that was not 
saved."""
+    async with Client(make_title_server()) as client:
+        result = await client.call_tool_mcp(
+            "update_dashboard", {"request": {"identifier": 1, alias: value}}
+        )
+    assert result.isError is True
+    assert "request.dashboard_title: Text must not be empty" in 
result.content[0].text
+
+
[email protected]("alias", ["chart_name", "name", "title", 
"slice_name"])
[email protected]("script_only", [True, False])
+def test_update_chart_sanitization_matches_generate(
+    alias: str, script_only: bool
+) -> None:
+    """Update rejects stripped-only names and warns on partially stripped 
names."""
+    if script_only:
+        with pytest.raises(ValidationError) as exc:
+            UpdateChartRequest.model_validate(
+                {"identifier": 1, alias: "<script>alert(1)</script>"}
+            )
+        assert exc.value.errors()[0]["type"] == "unsafe_text"
+        assert exc.value.errors()[0]["loc"] == ("chart_name",)
+        return
+    request = UpdateChartRequest.model_validate(
+        {
+            "identifier": 1,
+            alias: "Weekly KPIs <img src=x onerror=alert(1)>",
+            "sanitization_warnings": ["untrusted"],
+        }
+    )
+    assert request.chart_name == "Weekly KPIs"
+    assert len(request.sanitization_warnings) == 1
+    assert "untrusted" not in request.sanitization_warnings
+    assert "sanitization_warnings" not in json.dumps(
+        UpdateChartRequest.model_json_schema()
+    )
+
+
+def test_duplicate_dataset_error_is_readable() -> None:
+    """Duplicate errors use a plain string and preserve display-name 
characters."""
+    from superset.commands.dataset.exceptions import 
DatasetExistsValidationError
+    from superset.sql.parse import Table
+
+    name = "Sales “Q3” & Growth"
+    error = DatasetExistsValidationError(
+        Table(name, schema="my schema", catalog="my catalog")
+    )
+    assert error.normalized_messages() == {
+        "table": [f"Dataset my catalog.my schema.{name} already exists"]
+    }
+    assert type(error.messages[0]) is str
+
+
[email protected](
+    "kind,limit", [("dataset", 250), ("saved_query", 256), ("tab", 256)]
+)
[email protected]("extra", [0, 1])
+def test_padded_sibling_name_boundary(kind: str, limit: int, extra: int) -> 
None:
+    """Persisted names that trim must check the length after trimming."""
+    from superset.mcp_service.sql_lab.schemas import OpenSqlLabRequest
+
+    cases: dict[str, tuple[type[BaseModel], dict[str, Any], str]] = {
+        "dataset": (
+            CreateVirtualDatasetRequest,
+            {"database_id": 1, "sql": "select 1"},
+            "dataset_name",
+        ),
+        "saved_query": (
+            SaveSqlQueryRequest,
+            {"database_id": 1, "sql": "select 1"},
+            "label",
+        ),
+        "tab": (OpenSqlLabRequest, {"database_id": 1}, "title"),
+    }
+    model, payload, field = cases[kind]
+    value = "x" * (limit + extra)
+    if not extra:
+        assert (
+            getattr(model.model_validate({**payload, field: f"  {value}  "}), 
field)
+            == value
+        )
+    else:
+        with pytest.raises(ValidationError) as exc:
+            model.model_validate({**payload, field: f"  {value}  "})
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+
+
[email protected](
+    "model,payload",
+    [
+        (GenerateDashboardRequest, {"chart_ids": [1]}),
+        (UpdateDashboardRequest, {"identifier": 1}),
+        (DuplicateDashboardRequest, {"dashboard_id": 1}),
+    ],
+)
[email protected]("alias", ["dashboard_title", "title", "name"])
[email protected]("length", [500, 501])
+def test_padded_dashboard_title_boundary(
+    model: type[BaseModel], payload: dict[str, Any], alias: str, length: int
+) -> None:
+    """Dashboard aliases check the trimmed title against the column cap."""
+    if length == 500:
+        assert (
+            model.model_validate(
+                {**payload, alias: "  " + "x" * length + "  "}
+            ).dashboard_title
+            == "x" * length
+        )
+    else:
+        with pytest.raises(ValidationError) as exc:
+            model.model_validate({**payload, alias: "  " + "x" * length + "  
"})
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+
+
+def test_padded_dashboard_slug_boundary() -> None:
+    """Update slug normalization precedes its persisted column limit."""
+    request = UpdateDashboardRequest(identifier=1, slug="  " + "x" * 255 + "  
")
+    assert request.slug == "x" * 255
+    with pytest.raises(ValidationError):
+        UpdateDashboardRequest(identifier=1, slug="  " + "x" * 256 + "  ")
+
+
+_TITLE_SANITIZATION_CASES = [
+    (
+        GenerateChartRequest,
+        {
+            "dataset_id": 1,
+            "config": {"chart_type": "table", "columns": [{"name": "a"}]},
+            "save_chart": False,
+        },
+        "chart_name",
+    ),
+    (
+        GenerateChartRequest,
+        {
+            "dataset_id": 1,
+            "config": {"chart_type": "table", "columns": [{"name": "a"}]},
+        },
+        "chart_name",
+    ),
+    (UpdateChartRequest, {"identifier": 1}, "chart_name"),
+    (GenerateDashboardRequest, {"chart_ids": [1]}, "dashboard_title"),
+    (UpdateDashboardRequest, {"identifier": 1}, "dashboard_title"),
+    (DuplicateDashboardRequest, {"dashboard_id": 1}, "dashboard_title"),
+]
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
[email protected](
+    "value",
+    [
+        "Users aged <30",
+        "Revenue <$1M",
+        "Love <3 charts",
+        "Users aged <30 & “VIP” 📈",
+        "Users aged >30",
+        "Revenue\u00a0>\u00a0$1M",
+    ],
+)
+def test_literal_less_than_title_round_trip(
+    model: type[BaseModel], payload: dict[str, Any], field: str, value: str
+) -> None:
+    """Non-tag less-than signs survive validation and JSON readback 
unchanged."""
+    request = model.model_validate({**payload, field: value})
+    assert getattr(request, field) == value
+    assert request.sanitization_warnings == []
+    assert json.loads(request.model_dump_json())[field] == value
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
[email protected](
+    "value", ["Weekly <img src=x>KPIs", "Q3 <b>revenue</b> &amp; costs"]
+)
+def test_title_sanitized_once(
+    model: type[BaseModel], payload: dict[str, Any], field: str, value: str
+) -> None:
+    """Persist the single sanitizer result, never a second-pass empty 
fallback."""
+    from unittest.mock import patch
+
+    from superset.mcp_service.utils.sanitization import sanitize_user_input
+
+    expected = sanitize_user_input(value, "Title", max_length=500, 
allow_empty=True)
+    assert expected
+    with (
+        patch(
+            "superset.mcp_service.utils.sanitization.sanitize_user_input",
+            wraps=sanitize_user_input,
+        ) as sanitize,
+        patch("superset.mcp_service.chart.schemas.sanitize_user_input", 
sanitize),
+        patch("superset.mcp_service.dashboard.schemas.sanitize_user_input", 
sanitize),
+    ):
+        request = model.model_validate({**payload, field: value})
+    calls = [
+        c
+        for c in sanitize.call_args_list
+        if c.args[1] in ("Chart name", "Dashboard title")
+    ]
+    assert len(calls) == 1
+    assert getattr(request, field) == expected
+    assert len(request.sanitization_warnings) == 1
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
[email protected](
+    "value", ["&#x3c;&#x3c;</b>x", "&amp;lt;</script>click= <b>📈"]
+)
+def test_reconstructed_tag_title_rejected(
+    model: type[BaseModel], payload: dict[str, Any], field: str, value: str
+) -> None:
+    """Encoded tag text is rejected instead of stored or auto-named."""
+    with pytest.raises(ValidationError) as exc:
+        model.model_validate({**payload, field: value})
+    assert exc.value.errors()[0]["loc"] == (field,)
+    assert exc.value.errors()[0]["type"] == "unsafe_text"
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
+def test_empty_sanitized_title_never_uses_fallback(
+    model: type[BaseModel], payload: dict[str, Any], field: str
+) -> None:
+    """Encoded script-only names retain the existing disallowed-content 
reason."""
+    with pytest.raises(ValidationError) as exc:
+        model.model_validate({**payload, field: 
"&amp;lt;script>alert(1)</script>"})
+    assert exc.value.errors()[0]["loc"] == (field,)
+    assert exc.value.errors()[0]["type"] == "unsafe_text"
+
+
+_SIBLING_SANITIZATION_CASES = [
+    (ManageDashboardCertificationRequest, {"identifier": 1}, "certified_by"),
+    (
+        ManageDashboardCertificationRequest,
+        {"identifier": 1},
+        "certification_details",
+    ),
+    (BaseChartConfig, {}, "temporal_column"),
+    (ColumnRef, {}, "name"),
+    (ColumnRef, {"name": "amount"}, "label"),
+    (FilterConfig, {"op": "=", "value": "safe"}, "column"),
+    (FilterConfig, {"column": "region", "op": "="}, "value"),
+    (GanttSortByConfig, {}, "column"),
+    (
+        SunburstChartConfig,
+        {
+            "hierarchy": [{"name": "region"}],
+            "metric": {"name": "amount", "aggregate": "SUM"},
+        },
+        "time_range",
+    ),
+    (
+        InteractivePivotChartConfig,
+        {
+            "rows": [{"name": "region"}],
+            "metrics": [{"name": "amount", "aggregate": "SUM"}],
+        },
+        "comparison_period",
+    ),
+    (
+        BulletChartConfig,
+        {"metric": {"name": "amount", "aggregate": "SUM"}},
+        "time_range",
+    ),
+    (
+        BulletChartConfig,
+        {"metric": {"name": "amount", "aggregate": "SUM"}},
+        "range_labels",
+    ),
+    (
+        BulletChartConfig,
+        {"metric": {"name": "amount", "aggregate": "SUM"}},
+        "marker_labels",
+    ),
+    (
+        BulletChartConfig,
+        {"metric": {"name": "amount", "aggregate": "SUM"}},
+        "marker_line_labels",
+    ),
+]
+
+
[email protected]("model,payload,field", _SIBLING_SANITIZATION_CASES)
[email protected](
+    "value", ["Users aged <30", "Users aged >30", "Users\u00a0>30"]
+)
+def test_sibling_fields_preserve_literal_comparison(
+    model: type[BaseModel], payload: dict[str, Any], field: str, value: str
+) -> None:
+    """Every sibling accepts a non-tag comparison and preserves its literal 
text."""
+    if model is InteractivePivotChartConfig:
+        payload = {**payload, "comparison_type": "values"}
+    expected = [value] if field.endswith("_labels") else value
+    request = model.model_validate({**payload, field: expected})
+    assert getattr(request, field) == expected
+
+
[email protected]("model,payload,field", _SIBLING_SANITIZATION_CASES)
[email protected](
+    "value", ["&#x3c;&#x3c;</b>x", "&amp;lt;</script>click= <b>📈"]
+)
+def test_sibling_fields_reject_reconstructed_tag(
+    model: type[BaseModel], payload: dict[str, Any], field: str, value: str
+) -> None:
+    """Reconstructed tag openers fail with a canonical field and typed 
reason."""
+    if model is InteractivePivotChartConfig:
+        payload = {**payload, "comparison_type": "values"}
+    with pytest.raises(ValidationError) as exc:
+        model.model_validate(
+            {**payload, field: [value] if field.endswith("_labels") else value}
+        )
+    assert [(error["type"], error["loc"]) for error in exc.value.errors()] == [
+        ("unsafe_text", (field,))
+    ]
+
+
[email protected](
+    "model,payload,field",
+    [
+        (
+            CreateVirtualDatasetRequest,
+            {"database_id": 1, "sql": "select 1"},
+            "dataset_name",
+        ),
+        (UpdateDatasetRequest, {"dataset_id": 1}, "table_name"),
+        (
+            CreateDatasetMetricRequest,
+            {"dataset_id": 1, "expression": "SUM(x)"},
+            "metric_name",
+        ),
+        (UpdateDatasetMetricRequest, {"dataset_id": 1, "metric": 1}, 
"metric_name"),
+        (CreateThemeRequest, {"json_data": {}}, "theme_name"),
+        (SaveSqlQueryRequest, {"database_id": 1, "sql": "select 1"}, "label"),
+        (
+            CreateVirtualDatasetRequest,
+            {"database_id": 1, "sql": "select 1", "dataset_name": "ds"},
+            "description",
+        ),
+        (UpdateDatasetRequest, {"dataset_id": 1}, "description"),
+        (
+            SaveSqlQueryRequest,
+            {"database_id": 1, "sql": "select 1", "label": "query"},
+            "description",
+        ),
+        (UpdateDashboardRequest, {"identifier": 1}, "description"),
+    ],
+)
[email protected](
+    "value",
+    [
+        "Users aged <30",
+        "Revenue <$1M",
+        "Love <3 charts",
+        "&#x3c;&#x3c;</b>x",
+        "&amp;lt;</script>click= <b>📈",
+    ],
+)
+def test_unsanitized_sibling_names_remain_literal(
+    model: type[BaseModel], payload: dict[str, Any], field: str, value: str
+) -> None:
+    """Non-sanitizer paths retain their existing literal identifier/text 
contract."""
+    request = model.model_validate({**payload, field: value})
+    assert getattr(request, field) == value
+
+
[email protected](
+    "value",
+    [
+        "Users aged <30",
+        "Revenue <$1M",
+        "Love <3 charts",
+        "&#x3c;&#x3c;</b>x",
+        "&amp;lt;</script>click= <b>📈",
+    ],
+)
+def test_sql_lab_title_remains_literal(value: str) -> None:
+    """SQL Lab tab titles trim whitespace without HTML sanitization."""
+    from superset.mcp_service.sql_lab.schemas import OpenSqlLabRequest
+
+    assert OpenSqlLabRequest(database_id=1, title=value).title == value
+
+
[email protected]
[email protected]("field", ["dashboard_title", "title", "name"])
[email protected](
+    "rejected", ["<script>alert(1)</script>", "Weekly 
&am<b></b>p;lt;b&amp;gt;KPIs"]
+)
+async def test_duplicate_dashboard_removed_or_unstable_title_wire_reason(
+    field: str, rejected: str
+) -> None:
+    """Removed or unstable markup is unsafe content, not an empty input."""
+    async with Client(make_title_server()) as client:
+        result = await client.call_tool_mcp(
+            "duplicate_dashboard", {"request": {"dashboard_id": 1, field: 
rejected}}
+        )
+    assert result.isError is True
+    assert result.content == [
+        TextContent(
+            type="text",
+            text="Error: Validation error in duplicate_dashboard: "
+            "request.dashboard_title: Contains disallowed content "
+            "(HTML, script or data: URL, event handler, or SQL keyword); "
+            "a 'data:' label must not be followed by a comma",
+        )
+    ]
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
[email protected]("character", [">", "\u00a0"])
[email protected]("at_limit", [True, False])
+def test_title_length_counts_plain_text(
+    model: type[BaseModel],
+    payload: dict[str, Any],
+    field: str,
+    character: str,
+    at_limit: bool,
+) -> None:
+    """Entity serialization must not inflate persisted titles or reject valid 
ones."""
+    limit = 250 if field == "chart_name" else 500
+    value = "x" * (limit - 2 + (not at_limit)) + character + "y"
+    if not at_limit:
+        with pytest.raises(ValidationError) as exc:
+            model.model_validate({**payload, field: value})
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+        return
+    request = model.model_validate({**payload, field: value})
+    assert getattr(request, field) == value
+    assert len(getattr(request, field)) == limit
+    assert request.sanitization_warnings == []
+    assert getattr(model.model_validate(request.model_dump()), field) == value
+
+
+def test_builtin_name_validation_documented_for_administrators() -> None:
+    """Built-in validation guidance belongs with server administration."""
+    from pathlib import Path
+
+    root = Path(__file__).resolve().parents[3]
+    admin = (root / "docs/admin_docs/configuration/mcp-server.mdx").read_text()
+    extensions = (root / "docs/developer_docs/extensions/mcp.md").read_text()
+    for heading in ("Chart and dashboard titles", "Object-name validation"):
+        assert heading in admin
+        assert heading not in extensions
+    assert "Theme names retain their whitespace" in admin
+    assert "length validation applies after trimming" in admin
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
+def test_escaped_title_still_fits_model_column(
+    model: type[BaseModel], payload: dict[str, Any], field: str
+) -> None:
+    """Conservative bracket escaping cannot bypass a persisted title's length 
cap."""
+    limit = 250 if field == "chart_name" else 500
+    value = "x" * (limit - 2) + "<ſ"
+    with pytest.raises(ValidationError) as exc:
+        model.model_validate({**payload, field: value})
+    assert exc.value.errors()[0]["type"] == "string_too_long"
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
[email protected]("whitespace", ["\t", "\n", "\r", "\v", "\f", "\x85"])
[email protected](
+    "control",
+    ["!channel", "@U123", "#C123|general", "https://example.invalid|Open 
dashboard"],
+)
+def test_title_control_whitespace_cannot_form_slack_markup(
+    model: type[BaseModel],
+    payload: dict[str, Any],
+    field: str,
+    whitespace: str,
+    control: str,
+) -> None:
+    """Report title normalization must not join brackets into Slack 
controls."""
+    from superset.utils.file import sanitize_title
+
+    value = f"Q3 <{whitespace}{control}>"
+    request = model.model_validate({**payload, field: value})
+    stored = getattr(request, field)
+    assert "<" not in sanitize_title(stored)
+    assert getattr(model.model_validate(request.model_dump()), field) == stored
+
+
[email protected]("model,payload,field", _TITLE_SANITIZATION_CASES)
[email protected]("alias_index", range(4))
[email protected]("length_delta", [-5, 0, 1])
[email protected]("padding", ["", " ", "&#32;", "\u00a0"])
+def test_markup_title_length_uses_sanitized_text(
+    model: type[BaseModel],
+    payload: dict[str, Any],
+    field: str,
+    alias_index: int,
+    length_delta: int,
+    padding: str,
+) -> None:
+    """Markup is not part of the persisted title's character budget."""
+    aliases = (
+        ("chart_name", "name", "title", "slice_name")
+        if field == "chart_name"
+        else ("dashboard_title", "title", "name", "dashboard_title")
+    )
+    limit = 250 if field == "chart_name" else 500
+    text = "x" * (limit + length_delta)
+    value = "<b>" + padding + text + padding + "</b>"
+    if length_delta > 0:
+        with pytest.raises(ValidationError) as exc:
+            model.model_validate({**payload, aliases[alias_index]: value})
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+    else:
+        request = model.model_validate({**payload, aliases[alias_index]: 
value})
+        assert getattr(request, field) == text
+        assert len(request.sanitization_warnings) == 1
+
+
[email protected](
+    "model,payload,field",
+    [case for case in _SIBLING_SANITIZATION_CASES if case[2] != "value"],
+)
[email protected]("extra", [0, 1])
[email protected]("padding", ["", " ", "&#32;", "\u00a0"])
+def test_sibling_sanitized_length_budget(
+    model: type[BaseModel],
+    payload: dict[str, Any],
+    field: str,
+    extra: int,
+    padding: str,
+) -> None:
+    """All callers of the shared text sanitizer enforce the final text cap."""
+    limits = {
+        "certified_by": 500,
+        "certification_details": 5000,
+        "temporal_column": 255,
+        "name": 255,
+        "label": 500,
+        "column": 255,
+        "time_range": 1000,
+        "comparison_period": 100,
+        "range_labels": 200,
+        "marker_labels": 200,
+        "marker_line_labels": 200,
+    }
+    if model is InteractivePivotChartConfig:
+        payload = {**payload, "comparison_type": "values"}
+    text = "x" * (limits[field] + extra)
+    value = "<b>" + padding + text + padding + "</b>"
+    if extra:
+        with pytest.raises(ValidationError) as exc:
+            model.model_validate(
+                {**payload, field: [value] if field.endswith("_labels") else 
value}
+            )
+        assert exc.value.errors()[0]["type"] == "string_too_long"
+    else:
+        request = model.model_validate(
+            {**payload, field: [value] if field.endswith("_labels") else value}
+        )
+        assert getattr(request, field) == (
+            [text] if field.endswith("_labels") else text
+        )
+
+
[email protected](
+    "model,payload,field",
+    [
+        case
+        for case in _SIBLING_SANITIZATION_CASES
+        if case[2] != "value" and not case[2].endswith("_labels")
+    ],
+)
[email protected]("value", [b"<b>Safe</b>", bytearray(b"<b>Safe</b>")])

Review Comment:
   The remaining namespace byte-input gap is fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852. `CreateDatasetRequest.schema_` 
(including the `schema` alias) and `catalog` decode UTF-8 bytes/bytearray 
before trimming, blank-to-None normalization, and the 250-character cap. 
Invalid UTF-8 chart/dashboard titles retain a field-located `string_unicode` 
error. The audit also found and corrected the same string-only bypass in 
`GetChartInfoRequest.permalink_key` URL extraction.
   
   Regression tests in 
tests/unit_tests/mcp_service/test_sanitization_warning_inputs.py, run against 
untouched source at 746c57391cecb0fba4771a678914bfa1131efd73 and this commit:
   - test_byte_dataset_namespace_normalizes_before_length: 24 fail before / 
pass after; covers padded values, short/251-character blanks, exact-limit 
names, both byte types, both namespace fields, and the schema alias. It also 
checks the exact properties passed to dataset creation and omission of blank 
namespaces.
   - test_invalid_utf8_title_error_is_field_located: 48 fail before / pass 
after across all title-writing models and aliases, including saved/unsaved 
chart generation.
   - test_byte_permalink_url_extracts_key and 
test_byte_permalink_rejects_other_url: 4 fail before / pass after, covering the 
success and rejection paths.
   - test_dataset_namespace_rejects_malformed_values: 21 passing controls 
before and after, preserving type rejection, field locations, and the real 
over-limit cap.
   
   Final regression comparison: 76 failed / 21 passed before; 97 passed after. 
Full touched files: 1,633 passed. Changed-file pre-commit and explicit mypy 
passed; the branch's three frontend Jest suites passed (32 tests). Real-LLM 
checks excluded (OpenRouter disabled).
   
   I searched every `@field_validator` in `superset/mcp_service/**/schemas.py` 
with an AST inventory (88 validators), plus `rg -n 
'validator|isinstance.*str|decode\(' superset/mcp_service -g schemas.py` and 
callers of `CreateDatasetRequest`, `GetChartInfoRequest`, and 
`field_validation_error`. Full sibling audit follows; earlier already-decoding 
paths are distinguished from the namespace/title/permalink changes in this 
commit.
   
   Siblings checked:
   - superset/mcp_service/catalog/schemas.py:79 — not affected: 
GetCatalogRequest._normalize_search: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/catalog/schemas.py:87 — not affected: 
GetCatalogRequest._normalize_cursor: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:452 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/chart/schemas.py:484 — not affected: 
GetChartInfoRequest._parse_select_columns: list parser; bare bytes are rejected 
by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/chart/schemas.py:879 — not affected: 
BaseChartConfig.sanitize_temporal_column: already decodes bytes/bytearray 
before text normalization; field-validator errors already carry a field 
location.
   - superset/mcp_service/chart/schemas.py:1005 — not affected: 
ColumnRef.sanitize_name: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1017 — not affected: 
ColumnRef.sanitize_label: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1027 — not affected: 
ColumnRef.sanitize_sql: runs after Pydantic validates/coerces its declared 
field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:1140 — not affected: 
FilterConfig.sanitize_column: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1152 — not affected: 
FilterConfig.sanitize_value: runs after Pydantic validates/coerces its declared 
field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:1223 — not affected: 
GanttSortByConfig.sanitize_column: already decodes bytes/bytearray before text 
normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:1547 — not affected: 
GaugeChartConfig.validate_gauge_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:1712 — not affected: 
TreemapChartUpdateConfig.validate_treemap_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:2335 — not affected: 
SunburstLegacySavedFields.validate_legacy_limit: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:2777 — not affected: 
SunburstChartConfig.sanitize_time_range: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:3173 — not affected: 
InteractivePivotChartConfig.sanitize_comparison_period: already decodes 
bytes/bytearray before text normalization; field-validator errors already carry 
a field location.
   - superset/mcp_service/chart/schemas.py:3340 — not affected: 
MixedTimeseriesChartConfig.wrap_single_group_by: list/model shorthand; byte 
input cannot bypass into a stored string because the target list/model 
validation rejects it.
   - superset/mcp_service/chart/schemas.py:4080 — not affected: 
XYChartConfig.coerce_sort_by: list/model shorthand; byte input cannot bypass 
into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4105 — not affected: 
XYChartConfig.coerce_series_ranking_metric: list/model shorthand; byte input 
cannot bypass into a stored string because the target list/model validation 
rejects it.
   - superset/mcp_service/chart/schemas.py:4117 — not affected: 
XYChartConfig.validate_series_ranking_metric: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:4129 — not affected: 
XYChartConfig.wrap_single_group_by: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4134 — not affected: 
XYChartConfig.coerce_x_column_name: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4140 — not affected: 
XYChartConfig.coerce_y_column_names: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:4156 — not affected: 
XYChartConfig.coerce_legend_flag: boolean-to-model shorthand, not a string-only 
normalizer.
   - superset/mcp_service/chart/schemas.py:4611 — not affected: 
BulletChartConfig.tokenize_native_numeric_lists: native list shorthand; bare 
bytes are rejected by the list field, and label list elements sanitize after 
string coercion.
   - superset/mcp_service/chart/schemas.py:4623 — not affected: 
BulletChartConfig.tokenize_native_label_lists: native list shorthand; bare 
bytes are rejected by the list field, and label list elements sanitize after 
string coercion.
   - superset/mcp_service/chart/schemas.py:4633 — not affected: 
BulletChartConfig.reject_non_finite_values: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:4640 — not affected: 
BulletChartConfig.validate_presentation_labels: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:4660 — not affected: 
BulletChartConfig.sanitize_time_range: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/chart/schemas.py:5625 — not affected: 
GanttChartConfig.coerce_single_column: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:5631 — not affected: 
GanttChartConfig.coerce_tooltip_columns: list/model shorthand; byte input 
cannot bypass into a stored string because the target list/model validation 
rejects it.
   - superset/mcp_service/chart/schemas.py:5638 — not affected: 
GanttChartConfig.parse_native_order_by: list/model shorthand; byte input cannot 
bypass into a stored string because the target list/model validation rejects it.
   - superset/mcp_service/chart/schemas.py:5674 — not affected: 
GanttChartConfig.validate_gantt_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:5679 — not affected: 
GanttChartConfig.validate_time_bounds: runs after Pydantic validates/coerces 
its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:5805 — not affected: 
GeographicChartConfig.validate_geographic_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:6426 — not affected: 
ChartNameMixin.normalize_blank_chart_name: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/chart/schemas.py:7297 — not affected: 
RestoreChartRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/chart/schemas.py:7339 — not affected: 
DeleteChartRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:371 — not affected: 
GetDashboardInfoRequest._parse_select_columns: list parser; bare bytes are 
rejected by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/dashboard/schemas.py:913 — not affected: 
GenerateDashboardRequest.normalize_blank_dashboard_title: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:1084 — not affected: 
UpdateDashboardRequest.normalize_blank_dashboard_title: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:1094 — not affected: 
UpdateDashboardRequest.normalize_slug: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/dashboard/schemas.py:1195 — not affected: 
ManageDashboardOwnersRequest.reject_bool_identifier: boolean-ID/numeric guard, 
not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1204 — not affected: 
ManageDashboardOwnersRequest.reject_bool_owner_ids: boolean-ID/numeric guard, 
not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1294 — not affected: 
ManageDashboardRolesRequest.reject_bool_identifier: boolean-ID/numeric guard, 
not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1303 — not affected: 
ManageDashboardRolesRequest.reject_bool_role_ids: boolean-ID/numeric guard, not 
a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1394 — not affected: 
ManageDashboardCertificationRequest.reject_bool_identifier: boolean-ID/numeric 
guard, not a string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:1403 — not affected: 
ManageDashboardCertificationRequest.sanitize_certified_by: already decodes 
bytes/bytearray before text normalization; field-validator errors already carry 
a field location.
   - superset/mcp_service/dashboard/schemas.py:1427 — not affected: 
ManageDashboardCertificationRequest.sanitize_certification_details: already 
decodes bytes/bytearray before text normalization; field-validator errors 
already carry a field location.
   - superset/mcp_service/dashboard/schemas.py:1581 — not affected: 
DuplicateDashboardRequest.reject_blank_dashboard_title: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2315 — not affected: 
DeleteDashboardRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:2380 — not affected: 
BaseNewFilterSpec.reject_bool_dataset_id: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:2448 — not affected: 
FilterTimeSpec._validate_default_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2582 — not affected: 
NativeFilterUpdateSpec._validate_default_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2596 — not affected: 
NativeFilterUpdateSpec.reject_bool_dataset_id: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:2773 — not affected: 
HeaderComponentSpec.sanitize_text: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:2834 — not affected: 
DashboardComponentUpdateSpec.sanitize_text: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:3005 — not affected: 
ApplyFilterValueSpec._validate_time_range: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:3011 — not affected: 
ApplyFilterValueSpec._validate_range_order: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dashboard/schemas.py:3483 — not affected: 
RestoreDashboardRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dashboard/schemas.py:3570 — not affected: 
GetDashboardDataRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:344 — not affected: 
ListDatasetsRequest.parse_filters: passthrough with no string-only 
normalization.
   - superset/mcp_service/dataset/schemas.py:349 — not affected: 
ListDatasetsRequest.parse_select_columns: passthrough with no string-only 
normalization.
   - superset/mcp_service/dataset/schemas.py:439 — not affected: 
GetDatasetInfoRequest._parse_select_columns: list parser; bare bytes are 
rejected by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/dataset/schemas.py:449 — not affected: 
GetDatasetInfoRequest._parse_column_fields: list parser; bare bytes are 
rejected by the list field rather than persisted as unnormalized text.
   - superset/mcp_service/dataset/schemas.py:474 — not affected: 
CreateDatasetMetric.expression_must_aggregate: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:485 — not affected: 
CreateDatasetMetric.d3format_must_be_valid: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:556 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dataset/schemas.py:571 — not affected: 
CreateDatasetRequest._strip_table_name: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/dataset/schemas.py:627 — not affected: 
CreateVirtualDatasetRequest.sql_must_not_be_empty: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:634 — not affected: 
CreateVirtualDatasetRequest.dataset_name_must_not_be_empty: already decodes 
bytes/bytearray before text normalization; field-validator errors already carry 
a field location.
   - superset/mcp_service/dataset/schemas.py:749 — not affected: 
DatasetMetricProperties.d3format_must_be_valid: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/dataset/schemas.py:912 — not affected: 
DeleteDatasetRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:973 — not affected: 
RestoreDatasetRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:1056 — not affected: 
UpdateDatasetRequest.reject_bool_identifier: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:1065 — not affected: 
UpdateDatasetRequest.reject_bool_cache_timeout: boolean-ID/numeric guard, not a 
string-only text normalizer.
   - superset/mcp_service/dataset/schemas.py:1257 — not affected: 
QueryDatasetRequest._validate_time_range: runs after Pydantic validates/coerces 
its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/semantic_layer/schemas.py:273 — not affected: 
GetTableRequest.normalize_time_grain: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/semantic_layer/schemas.py:309 — not affected: 
GetTableRequest._validate_time_range: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/sql_lab/schemas.py:108 — not affected: 
ExecuteSqlRequest.sql_not_empty: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/sql_lab/schemas.py:123 — not affected: 
ColumnInfo.coerce_is_nullable: response metadata coercion, not a request 
normalization path.
   - superset/mcp_service/sql_lab/schemas.py:263 — not affected: 
SaveSqlQueryRequest.sql_not_empty: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/sql_lab/schemas.py:270 — not affected: 
SaveSqlQueryRequest.label_not_empty: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/sql_lab/schemas.py:335 — not affected: 
OpenSqlLabRequest.title_strip_or_none: already decodes bytes/bytearray before 
text normalization; field-validator errors already carry a field location.
   - superset/mcp_service/system/schemas.py:219 — not affected: 
FindUsersRequest._reject_blank_query: runs after Pydantic validates/coerces its 
declared field, so bytes cannot skip the validator.
   - superset/mcp_service/system/schemas.py:230 — not affected: 
FindUsersRequest._reject_email_shaped_query: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/theme/schemas.py:155 — not affected: 
CreateThemeRequest.reject_blank_theme_name: runs after Pydantic 
validates/coerces its declared field, so bytes cannot skip the validator.
   - superset/mcp_service/user/schemas.py:104 — not affected: 
UserInfo._extract_role_names: response metadata coercion, not a request 
normalization path.
   - superset/mcp_service/chart/schemas.py:6373 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dashboard/schemas.py:859 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dashboard/schemas.py:1031 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dashboard/schemas.py:1527 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/utils/schema_utils.py:41 — fixed in 
705c6f0bc395800d6b5fee03d5bef0a9830b1852
   - superset/mcp_service/dataset/schemas.py:580 — not affected: 
CreateVirtualDatasetRequest schema/catalog do not promise trim/blank 
normalization for string inputs either.
   - superset/mcp_service/dataset/schemas.py:701 — not affected: 
DatasetMetricProperties and its create/update subclasses validate literal 
metric names after coercion; they deliberately retain whitespace.
   - superset/mcp_service/dataset/schemas.py:1012 — not affected: 
UpdateDatasetRequest validates its literal table_name after coercion; it 
deliberately retains whitespace.
   - superset/mcp_service/dataset/schemas.py:881 — not affected: slice_name is 
response metadata, not a title-writing request.
   - superset/mcp_service/chart/schemas.py:6469 — not affected: 
GenerateExploreLinkRequest has no chart-name field.
   - superset/mcp_service/chart/schemas.py:6576 — not affected: 
UpdateChartPreviewRequest has no chart-name field.
   - superset/mcp_service/dataset/tool/create_dataset.py:97 — not affected: 
_build_dataset_properties consumes the normalized typed request; the new 
namespace regression also asserts the exact persisted properties and omitted 
blank keys.
   - superset/mcp_service/chart/tool/get_chart_info.py:550 — not affected: the 
cache loader consumes the validated permalink key; it has no separate raw-input 
decoder.
   - superset/mcp_service/chart/tool/generate_chart.py:255 — not affected: 
revalidation carries the already-sanitized chart_name; no second raw-byte entry 
point.
   - superset/mcp_service/chart/tool/update_chart.py — not affected: consumes 
UpdateChartRequest, which inherits the shared chart-name guard.
   - superset/mcp_service/dashboard/tool/generate_dashboard.py — not affected: 
consumes GenerateDashboardRequest; shared schema owns byte decoding.
   - superset/mcp_service/dashboard/tool/update_dashboard.py — not affected: 
consumes UpdateDashboardRequest; shared schema owns byte decoding.
   - superset/mcp_service/dashboard/tool/duplicate_dashboard.py — not affected: 
consumes DuplicateDashboardRequest; shared schema owns byte decoding.
   - superset/mcp_service/utils/validation.py:251 — not affected: formats only 
schema-backed locations and static reasons, without msg, ctx, or input.
   



-- 
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