-
Notifications
You must be signed in to change notification settings - Fork 17.2k
fix(mcp): trim get_dataset_info response to prevent oversized payloads #39898
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
b5ed6b6
b888574
b297e39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -93,6 +93,29 @@ class TableColumnInfo(BaseModel): | |||||||||||||||||||||||||||||||||||||
| filterable: bool | None = Field(None, description="Is filterable") | ||||||||||||||||||||||||||||||||||||||
| description: str | None = Field(None, description="Column description") | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @model_serializer(mode="wrap") | ||||||||||||||||||||||||||||||||||||||
| def _filter_column_fields_by_context( | ||||||||||||||||||||||||||||||||||||||
| self, serializer: Any, info: Any | ||||||||||||||||||||||||||||||||||||||
| ) -> Dict[str, Any]: | ||||||||||||||||||||||||||||||||||||||
| """Filter column fields based on serialization context. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| If context contains 'column_fields', only include those fields. | ||||||||||||||||||||||||||||||||||||||
| Otherwise, include all fields. This trims wide datasets so a | ||||||||||||||||||||||||||||||||||||||
| 50-column dataset doesn't ship 50 long descriptions when the | ||||||||||||||||||||||||||||||||||||||
| caller only needs column_name + type. | ||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| data = serializer(self) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if info.context and isinstance(info.context, dict): | ||||||||||||||||||||||||||||||||||||||
| column_fields = info.context.get("column_fields") | ||||||||||||||||||||||||||||||||||||||
| if column_fields: | ||||||||||||||||||||||||||||||||||||||
| requested = set(column_fields) | ||||||||||||||||||||||||||||||||||||||
| # Always preserve column_name as the only required field | ||||||||||||||||||||||||||||||||||||||
| requested.add("column_name") | ||||||||||||||||||||||||||||||||||||||
| return {k: v for k, v in data.items() if k in requested} | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return data | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+109
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect Filtering Logic
The serializer skips filtering when Code suggestionCheck the AI-generated fix before applying
Suggested change
Code Review Run #f4f326 Should Bito avoid suggestions like this for future reviews? (Manage Rules)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already addressed via a different layer in b888574 — the |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class SqlMetricInfo(BaseModel): | ||||||||||||||||||||||||||||||||||||||
| metric_name: str = Field( | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -311,13 +334,89 @@ def create(cls, error: str, error_type: str) -> "DatasetError": | |||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| DEFAULT_GET_DATASET_INFO_COLUMNS: List[str] = [ | ||||||||||||||||||||||||||||||||||||||
| "id", | ||||||||||||||||||||||||||||||||||||||
| "table_name", | ||||||||||||||||||||||||||||||||||||||
| "schema", | ||||||||||||||||||||||||||||||||||||||
| "database_name", | ||||||||||||||||||||||||||||||||||||||
| "database_id", | ||||||||||||||||||||||||||||||||||||||
| "uuid", | ||||||||||||||||||||||||||||||||||||||
| "is_virtual", | ||||||||||||||||||||||||||||||||||||||
| "description", | ||||||||||||||||||||||||||||||||||||||
| "main_dttm_col", | ||||||||||||||||||||||||||||||||||||||
| "sql", | ||||||||||||||||||||||||||||||||||||||
| "url", | ||||||||||||||||||||||||||||||||||||||
| "columns", | ||||||||||||||||||||||||||||||||||||||
| "metrics", | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS: List[str] = [ | ||||||||||||||||||||||||||||||||||||||
| "column_name", | ||||||||||||||||||||||||||||||||||||||
| "type", | ||||||||||||||||||||||||||||||||||||||
| "is_dttm", | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class GetDatasetInfoRequest(MetadataCacheControl): | ||||||||||||||||||||||||||||||||||||||
| """Request schema for get_dataset_info with support for ID or UUID.""" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| identifier: Annotated[ | ||||||||||||||||||||||||||||||||||||||
| int | str, | ||||||||||||||||||||||||||||||||||||||
| Field(description="Dataset identifier - can be numeric ID or UUID string"), | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| select_columns: Annotated[ | ||||||||||||||||||||||||||||||||||||||
| List[str], | ||||||||||||||||||||||||||||||||||||||
| Field( | ||||||||||||||||||||||||||||||||||||||
| default_factory=lambda: list(DEFAULT_GET_DATASET_INFO_COLUMNS), | ||||||||||||||||||||||||||||||||||||||
| description=( | ||||||||||||||||||||||||||||||||||||||
| "Top-level fields to include in the response. Defaults to a lean " | ||||||||||||||||||||||||||||||||||||||
| "set that excludes verbose fields like params, template_params, " | ||||||||||||||||||||||||||||||||||||||
| "extra, tags, certification_details. Pass an explicit list to " | ||||||||||||||||||||||||||||||||||||||
| "override (e.g. ['id','table_name','columns'] for minimal output)." | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
| column_fields: Annotated[ | ||||||||||||||||||||||||||||||||||||||
| List[str], | ||||||||||||||||||||||||||||||||||||||
| Field( | ||||||||||||||||||||||||||||||||||||||
| default_factory=lambda: list(DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS), | ||||||||||||||||||||||||||||||||||||||
| description=( | ||||||||||||||||||||||||||||||||||||||
| "Per-column fields to include for entries in 'columns'. Defaults " | ||||||||||||||||||||||||||||||||||||||
| "to ['column_name','type','is_dttm']. Pass a wider list to " | ||||||||||||||||||||||||||||||||||||||
| "include 'verbose_name','groupby','filterable','description' " | ||||||||||||||||||||||||||||||||||||||
| "when needed. Trimming per-column fields keeps responses small " | ||||||||||||||||||||||||||||||||||||||
| "for wide datasets." | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @field_validator("select_columns", mode="before") | ||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||
| def _parse_select_columns(cls, value: Any) -> Any: | ||||||||||||||||||||||||||||||||||||||
| from superset.mcp_service.utils.schema_utils import parse_json_or_list | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if value is None: | ||||||||||||||||||||||||||||||||||||||
| return list(DEFAULT_GET_DATASET_INFO_COLUMNS) | ||||||||||||||||||||||||||||||||||||||
| parsed = parse_json_or_list(value, "select_columns") | ||||||||||||||||||||||||||||||||||||||
| # Treat empty list as "use defaults" so callers cannot accidentally | ||||||||||||||||||||||||||||||||||||||
| # opt out of size reduction by passing []. Without this, an empty | ||||||||||||||||||||||||||||||||||||||
| # list disables filtering downstream and reintroduces oversized | ||||||||||||||||||||||||||||||||||||||
| # responses. | ||||||||||||||||||||||||||||||||||||||
| if not parsed: | ||||||||||||||||||||||||||||||||||||||
| return list(DEFAULT_GET_DATASET_INFO_COLUMNS) | ||||||||||||||||||||||||||||||||||||||
| return parsed | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @field_validator("column_fields", mode="before") | ||||||||||||||||||||||||||||||||||||||
| @classmethod | ||||||||||||||||||||||||||||||||||||||
| def _parse_column_fields(cls, value: Any) -> Any: | ||||||||||||||||||||||||||||||||||||||
| from superset.mcp_service.utils.schema_utils import parse_json_or_list | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if value is None: | ||||||||||||||||||||||||||||||||||||||
| return list(DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS) | ||||||||||||||||||||||||||||||||||||||
| parsed = parse_json_or_list(value, "column_fields") | ||||||||||||||||||||||||||||||||||||||
| if not parsed: | ||||||||||||||||||||||||||||||||||||||
| return list(DEFAULT_GET_DATASET_INFO_COLUMN_FIELDS) | ||||||||||||||||||||||||||||||||||||||
| return parsed | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class CreateVirtualDatasetRequest(BaseModel): | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: The column filtering check treats an explicitly provided empty list as "no filter" and returns all column fields. This is a logic bug because callers can pass
column_fields=[](or values that parse to an empty list) and unexpectedly get verbose fields likedescriptionfor every column, which defeats the payload-size reduction and can reintroduce oversized responses/timeouts. Handle empty lists as a valid filter input (e.g., still enforce the minimal required field set) instead of falling back to full serialization. [logic error]Severity Level: Critical 🚨
Steps of Reproduction ✅
Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Fixed in b888574 — both
select_columns=[]andcolumn_fields=[]now coerce to the lean default in the field validators rather than falling through to "no filter". Added a regression test (test_get_dataset_info_empty_lists_fall_back_to_defaults) that passes an empty list and asserts the lean defaults are still applied.