Skip to content

feat(mcp): add update_dataset_metric tool for editing saved dataset metrics - #40975

Merged
msyavuz merged 4 commits into
apache:masterfrom
msyavuz:msyavuz/feat/mcp-update-dataset-metric
Jul 13, 2026
Merged

feat(mcp): add update_dataset_metric tool for editing saved dataset metrics#40975
msyavuz merged 4 commits into
apache:masterfrom
msyavuz:msyavuz/feat/mcp-update-dataset-metric

Conversation

@msyavuz

@msyavuz msyavuz commented Jun 11, 2026

Copy link
Copy Markdown
Member

SUMMARY

MCP exposes saved dataset metrics read-only via get_dataset_info, but there is no write path — editing a metric's SQL expression, name, label, or format requires the UI. This adds an update_dataset_metric tool: given a dataset (ID/UUID) and a metric (ID/UUID/name), it partially updates the metric's properties (expression, metric_name, verbose_name, description, d3format, metric_type, currency, warning_text, extra).

Design notes:

  • A dedicated tool rather than a generic dataset-PUT: DatasetDAO.update_metrics is full-replacement (metrics omitted from the array are deleted), which is an easy foot-gun for LLM callers. The tool rebuilds the full metrics list internally — untouched metrics as {id, metric_name} stubs, the target with only the explicitly-passed properties merged on top — so it can never create or delete metrics.
  • Goes through UpdateDatasetCommand, which enforces dataset ownership and validates name uniqueness and the SQL expression (validate_stored_expression).
  • exclude_unset semantics distinguish "not provided" (keep stored value) from explicit null (clear it).
  • Also extracts the int-or-UUID dataset resolver from query_dataset into a shared dataset_utils.resolve_dataset.

Example call:

{"dataset_id": 123, "metric": "sum_revenue", "expression": "SUM(net_revenue)", "d3format": "$,.2f"}

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — MCP tool, no UI.

TESTING INSTRUCTIONS

  • pytest tests/unit_tests/mcp_service/dataset/ (97 tests)
  • Manually: connect an MCP client, call update_dataset_metric with a dataset ID and metric name, confirm the change in the dataset editor and that other metrics are untouched.

ADDITIONAL INFORMATION

  • Has associated issue: No
  • Required feature flags: None
  • Changes UI: No
  • Includes DB Migration: No
  • Introduces new feature or API: new MCP tool update_dataset_metric
  • Removes existing feature or API: No

@dosubot dosubot Bot added the data:dataset Related to dataset configurations label Jun 11, 2026
Comment on lines +25 to +44
def resolve_dataset(
identifier: int | str, eager_options: list[Any] | None = None
) -> Any | None:
"""Resolve a dataset by int ID or UUID string.

Replicates the identifier resolution logic from ModelGetInfoCore._find_object().
"""
from superset.daos.dataset import DatasetDAO

opts = eager_options or None

if isinstance(identifier, int):
return DatasetDAO.find_by_id(identifier, query_options=opts)

# Try parsing as int
try:
id_val = int(identifier)
return DatasetDAO.find_by_id(id_val, query_options=opts)
except (ValueError, TypeError):
pass

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Restrict dataset lookup inputs to UUID-only identifiers and remove integer-ID resolution paths so this new helper does not expose internal numeric IDs through a public MCP-facing flow. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This new helper explicitly accepts and resolves integer IDs before UUIDs, which exposes internal numeric identifiers through a public MCP-facing lookup path.
That matches the custom rule about not introducing internal integer IDs when UUID-based identifiers are being used.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/dataset_utils.py
**Line:** 25:44
**Comment:**
	*Custom Rule: Restrict dataset lookup inputs to UUID-only identifiers and remove integer-ID resolution paths so this new helper does not expose internal numeric IDs through a public MCP-facing flow.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +62 to +70
def _metric_not_found_message(metrics: list[Any], identifier: int | str) -> str:
names = [m.metric_name for m in metrics]
msg = f"Metric '{identifier}' not found on this dataset."
if not names:
return f"{msg} This dataset has no saved metrics."
suggestions = difflib.get_close_matches(str(identifier), names, n=3, cutoff=0.6)
if suggestions:
return f"{msg} Did you mean: {', '.join(suggestions)}?"
return f"{msg} Available metrics: {', '.join(sorted(names))}."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a concise docstring to this newly added helper function so it is documented consistently with the other new functions in the file. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is a newly added Python helper function and it does not include a docstring. The custom rule requires new functions and classes to be documented inline, so the suggestion accurately identifies a real violation.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/tool/update_dataset_metric.py
**Line:** 62:70
**Comment:**
	*Custom Rule: Add a concise docstring to this newly added helper function so it is documented consistently with the other new functions in the file.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +39 to +40
def mcp_server():
return mcp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an explicit return type annotation to this fixture function so new Python code is fully typed. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is a new Python function in the PR and it has no return type annotation.
The rule requires new Python code to be fully typed, so this is a real violation.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 39:40
**Comment:**
	*Custom Rule: Add an explicit return type annotation to this fixture function so new Python code is fully typed.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎



@pytest.fixture(autouse=True)
def mock_auth():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Annotate the return type of this yielding fixture (for example as a generator type) to satisfy the full type-hint requirement for new functions. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This new fixture function lacks a return type annotation. Under the typing rule for new Python code, that omission is a genuine violation.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 44:44
**Comment:**
	*Custom Rule: Annotate the return type of this yielding fixture (for example as a generator type) to satisfy the full type-hint requirement for new functions.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +56 to +61
def make_metric(
metric_id=10,
metric_name="count",
uuid="a1b2c3d4-5678-90ab-cdef-1234567890ab",
**overrides,
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add type hints for all parameters and the return value of this helper to comply with the fully typed new-code rule. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The helper is newly added and its parameters and return value are untyped.
That directly violates the requirement that new Python code be fully typed.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 56:61
**Comment:**
	*Custom Rule: Add type hints for all parameters and the return value of this helper to comply with the fully typed new-code rule.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

return metric


def make_dataset(dataset_id=1, metrics=None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add parameter and return type annotations to this helper function so it meets the typing requirement for newly added functions. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This new helper function has untyped parameters and no return annotation, so it does violate the new-code typing rule.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 79:79
**Comment:**
	*Custom Rule: Add parameter and return type annotations to this helper function so it meets the typing requirement for newly added functions.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@netlify

netlify Bot commented Jun 11, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 4077b01
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a341e96ab25aa000858289a
😎 Deploy Preview https://deploy-preview-40975--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Comment on lines +520 to +524
dataset_id: int | str = Field(
...,
description="Dataset identifier — numeric ID or UUID string. "
"Use list_datasets to find valid IDs.",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Restrict this request identifier to UUID-only input (or a dedicated UUID field) so the new public API does not continue accepting internal integer IDs. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is a new public API request field that explicitly accepts numeric internal IDs in addition to UUIDs. The rule says new public API identifiers should not expose or depend on internal integer IDs when UUID-based identifiers are being added or changed. This is a real violation.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 520:524
**Comment:**
	*Custom Rule: Restrict this request identifier to UUID-only input (or a dedicated UUID field) so the new public API does not continue accepting internal integer IDs.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +525 to +529
metric: int | str = Field(
...,
description="Metric to update — numeric metric ID, metric UUID, or "
"metric_name (e.g. 'sum_revenue'). Numeric strings are treated as IDs. "
"Use get_dataset_info to discover a dataset's saved metrics.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Remove numeric metric ID support from this API identifier and require UUID (or non-internal name-only lookup) to avoid exposing or depending on internal integer IDs in the new interface. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This request schema explicitly allows a numeric metric ID and even treats numeric strings as IDs, which exposes and depends on internal integer identifiers in a new public interface. That matches the rule violation.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 525:529
**Comment:**
	*Custom Rule: Remove numeric metric ID support from this API identifier and require UUID (or non-internal name-only lookup) to avoid exposing or depending on internal integer IDs in the new interface.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +592 to +593
id: int | None = Field(None, description="Metric ID")
uuid: str | None = Field(None, description="Metric UUID")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Drop the integer metric ID from the response model and expose only the UUID-based identifier for this newly added metric detail payload. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The response model includes an integer metric ID alongside a UUID field. Since this is a newly added public API payload and a UUID identifier is being introduced, exposing the internal integer ID violates the rule.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 592:593
**Comment:**
	*Custom Rule: Drop the integer metric ID from the response model and expose only the UUID-based identifier for this newly added metric detail payload.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

class UpdateDatasetMetricResponse(BaseModel):
"""Response schema for update_dataset_metric."""

dataset_id: int | None = Field(None, description="Dataset ID")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Replace this integer dataset identifier field with a UUID-based public identifier in the new response schema to avoid exposing internal numeric IDs. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This response schema exposes an internal integer dataset ID in a new public API response. That is exactly the kind of identifier the rule says to avoid when UUID-based identifiers are being added or changed.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 604:604
**Comment:**
	*Custom Rule: Replace this integer dataset identifier field with a UUID-based public identifier in the new response schema to avoid exposing internal numeric IDs.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.75000% with 106 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.01%. Comparing base (3c69822) to head (2ebaf3b).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../mcp_service/dataset/tool/update_dataset_metric.py 15.95% 79 Missing ⚠️
superset/mcp_service/dataset/schemas.py 70.83% 14 Missing ⚠️
superset/mcp_service/dataset/dataset_utils.py 20.00% 12 Missing ⚠️
superset/mcp_service/dataset/tool/query_dataset.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #40975      +/-   ##
==========================================
- Coverage   65.02%   65.01%   -0.01%     
==========================================
  Files        2742     2744       +2     
  Lines      153422   153567     +145     
  Branches    35198    35211      +13     
==========================================
+ Hits        99761    99840      +79     
- Misses      51752    51820      +68     
+ Partials     1909     1907       -2     
Flag Coverage Δ
hive 39.06% <33.75%> (-0.01%) ⬇️
mysql 57.92% <33.75%> (?)
postgres 57.98% <33.75%> (-0.05%) ⬇️
presto 41.05% <33.75%> (-0.02%) ⬇️
python 59.36% <33.75%> (-0.02%) ⬇️
sqlite 57.59% <33.75%> (-0.05%) ⬇️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread superset/mcp_service/dataset/schemas.py Outdated
Comment on lines +510 to +511
symbolPosition: Literal["prefix", "suffix"] | None = Field( # noqa: N815
None, description="Where to render the symbol relative to the value."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Restricting currency.symbolPosition to only "prefix"/"suffix" is incompatible with the existing dataset update contract, which accepts arbitrary non-empty strings (and legacy data may contain other values). This can cause valid update requests or serialization of existing metric currency payloads to fail with validation errors. Align this field type with the broader Superset schema behavior instead of enforcing a strict enum here. [api mismatch]

Severity Level: Major ⚠️
- ❌ update_dataset_metric fails for metrics with custom symbolPosition
- ⚠️ MCP clients see unexpected validation or internal errors
Steps of Reproduction ✅
1. Observe the existing REST dataset update contract in
`superset/datasets/schemas.py:17-20`, where
`DatasetMetricCurrencyPutSchema.symbolPosition` is defined as
`fields.String(validate=Length(1, 128))` with no enum restriction, and in the generated
OpenAPI docs (`docs/static/resources/openapi.json:5205-5208`) describing `symbolPosition`
as a generic string field. This confirms existing APIs accept arbitrary non-empty strings
(not just `"prefix"`/`"suffix"`).

2. Note that metric currency values are stored in the database via `CurrencyType` in
`superset/models/sql_types/base.py:67-86`, which parses legacy string values into dicts
using `parse_currency_string` without constraining `symbolPosition`. As a result, existing
datasets can legitimately contain arbitrary `currency={"symbol": "USD", "symbolPosition":
"before"}` or other non-enum values.

3. Call the MCP tool `update_dataset_metric` (entrypoint
`superset/mcp_service/dataset/tool/update_dataset_metric.py:113-229`) against any dataset
whose metric has such a currency dict (e.g. created previously via the REST API or UI), by
sending a minimal request that does not touch currency: `{"request": {"dataset_id": 1,
"metric": "count", "expression": "COUNT(1)"}}`. The tool loads the dataset metrics (lines
167-179) and locates the target metric (lines 179-183).

4. After `UpdateDatasetCommand.run()` returns the updated dataset (lines 207-213), the
tool serializes the metric via `_serialize_metric` at `update_dataset_metric.py:73-100`.
That calls `MetricCurrency.model_validate(currency)` when `currency` is a dict (lines
93-95); however `MetricCurrency.symbolPosition` is declared as `Literal["prefix",
"suffix"]` in `superset/mcp_service/dataset/schemas.py:35-43`, so any existing metric with
`symbolPosition` not equal to `"prefix"` or `"suffix"` causes a
`pydantic.ValidationError`. This bubbles into the broad `except Exception` block at lines
253-258, which logs `"Unexpected error updating dataset metric"` and re-raises, causing
the MCP tool call to fail with an internal error even though the underlying dataset update
may have succeeded.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 510:511
**Comment:**
	*Api Mismatch: Restricting `currency.symbolPosition` to only `"prefix"`/`"suffix"` is incompatible with the existing dataset update contract, which accepts arbitrary non-empty strings (and legacy data may contain other values). This can cause valid update requests or serialization of existing metric currency payloads to fail with validation errors. Align this field type with the broader Superset schema behavior instead of enforcing a strict enum here.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +589 to +598
class DatasetMetricDetail(SqlMetricInfo):
"""Full saved-metric details, including identifiers."""

id: int | None = Field(None, description="Metric ID")
uuid: str | None = Field(None, description="Metric UUID")
metric_type: str | None = Field(None, description="Metric type")
currency: MetricCurrency | None = Field(
None, description="Currency formatting configuration"
)
warning_text: str | None = Field(None, description="Warning text")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The response model for updated metrics omits the extra property even though the request allows updating it and the tool advertises returning the metric after update. This makes the response incomplete for one of the supported update fields and breaks callers that expect to read back the updated extra value. Add extra to the metric detail response schema so response content matches supported updates. [incomplete implementation]

Severity Level: Major ⚠️
- ⚠️ update_dataset_metric response omits updated extra metadata
- ⚠️ Callers cannot confirm saved metric extra changes
Steps of Reproduction ✅
1. Inspect the request schema for the MCP tool in
`superset/mcp_service/dataset/schemas.py:46-92`. `UpdateDatasetMetricRequest` includes
`extra: str | None = Field(None, description="JSON-encoded string with extra metric
metadata.")` (lines 88-92) and `UPDATABLE_METRIC_FIELDS` (lines 20-32) includes `"extra"`,
so callers are explicitly allowed to update `extra`.

2. Inspect the response-side metric model `DatasetMetricDetail` in the same file at
`superset/mcp_service/dataset/schemas.py:120-129`. It subclasses `SqlMetricInfo` (which
defines `metric_name`, `verbose_name`, `expression`, `description`, `d3format` at lines
41-50) and adds `id`, `uuid`, `metric_type`, `currency`, and `warning_text` (lines
123-129), but does not define an `extra` field.

3. Examine the tool implementation in
`superset/mcp_service/dataset/tool/update_dataset_metric.py:73-100`. `_serialize_metric()`
builds a `DatasetMetricDetail` from the ORM metric, setting `id`, `uuid`, `metric_name`,
`verbose_name`, `expression`, `description`, `d3format`, `metric_type`, `currency`, and
`warning_text`, but never includes `metric.extra` when constructing the response object.

4. From an MCP client, call the tool `update_dataset_metric` (entrypoint
`update_dataset_metric.py:113-229`) with a payload that only updates `extra`, for example:
`{"request": {"dataset_id": 1, "metric": "count", "extra": "{\"foo\": \"bar\"}"}}`. The
request validates and `UpdateDatasetMetricRequest.updates()` (lines 94-103) includes
`"extra"` in the metrics payload sent to `UpdateDatasetCommand`. After the update, the
tool returns `UpdateDatasetMetricResponse.metric` as a `DatasetMetricDetail` (lines
220-223), but because `DatasetMetricDetail` has no `extra` field and `_serialize_metric()`
never passes it through, the JSON response lacks any `metric["extra"]` field. Thus callers
that just updated `extra` cannot read back the new value via this tool, despite the schema
advertising that the metric is returned "after the update".

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 589:598
**Comment:**
	*Incomplete Implementation: The response model for updated metrics omits the `extra` property even though the request allows updating it and the tool advertises returning the metric after update. This makes the response incomplete for one of the supported update fields and breaks callers that expect to read back the updated `extra` value. Add `extra` to the metric detail response schema so response content matches supported updates.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review bito-code-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #513598

Actionable Suggestions - 1
  • superset/mcp_service/dataset/dataset_utils.py - 1
    • Missing unit tests for new utility · Line 25-50
Additional Suggestions - 2
  • tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py - 1
    • Missing error path test coverage · Line 253-378
      The test suite covers DatasetNotFoundError, DatasetForbiddenError, and DatasetInvalidError, but does not test DatasetUpdateFailedError which is handled at update_dataset_metric.py lines 248-252. Adding coverage ensures complete error path validation.
  • superset/mcp_service/app.py - 1
    • Documentation field name mismatch · Line 166-166
      The documentation says 'label' but the actual updatable field is named 'verbose_name' per the schema definition. Update the description to say 'verbose_name' for consistency.
      Code suggestion
      --- a/superset/mcp_service/app.py
      +++ b/superset/mcp_service/app.py
       @@ -163,7 +163,7 @@ Dataset Management:
      - list_datasets: List datasets with advanced filters (1-based pagination)
      - get_dataset_info: Get detailed dataset information by ID (includes columns/metrics)
      - create_virtual_dataset: Save a SQL query as a virtual dataset for charting (requires write access)
      -- update_dataset_metric: Update a saved metric on a dataset — expression, name, label, format (requires dataset ownership)
      +- update_dataset_metric: Update a saved metric on a dataset — expression, name, verbose_name, format (requires dataset ownership)
      - query_dataset: Query a dataset using its semantic layer (saved metrics, dimensions, filters) without needing a saved chart
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py - 1
  • superset/mcp_service/dataset/schemas.py - 1
    • Missing validation_alias for symbolPosition · Line 510-512
Review Details
  • Files reviewed - 8 · Commit Range: 2311482..2311482
    • superset/mcp_service/app.py
    • superset/mcp_service/dataset/dataset_utils.py
    • superset/mcp_service/dataset/schemas.py
    • superset/mcp_service/dataset/tool/__init__.py
    • superset/mcp_service/dataset/tool/query_dataset.py
    • superset/mcp_service/dataset/tool/update_dataset_metric.py
    • tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py
    • tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +25 to +50
def resolve_dataset(
identifier: int | str, eager_options: list[Any] | None = None
) -> Any | None:
"""Resolve a dataset by int ID or UUID string.

Replicates the identifier resolution logic from ModelGetInfoCore._find_object().
"""
from superset.daos.dataset import DatasetDAO

opts = eager_options or None

if isinstance(identifier, int):
return DatasetDAO.find_by_id(identifier, query_options=opts)

# Try parsing as int
try:
id_val = int(identifier)
return DatasetDAO.find_by_id(id_val, query_options=opts)
except (ValueError, TypeError):
pass

# Try UUID
if _is_uuid(str(identifier)):
return DatasetDAO.find_by_id(identifier, id_column="uuid", query_options=opts)

return None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing unit tests for new utility

The resolve_dataset function is new but lacks dedicated unit tests. Per BITO.md rule [11730], new tools/functions require comprehensive unit tests covering success paths, error scenarios, and edge cases. Tests should be placed in tests/unit_tests/mcp_service/dataset/ following existing MCP test patterns.

Code Review Run #513598


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@msyavuz
msyavuz force-pushed the msyavuz/feat/mcp-update-dataset-metric branch from 2311482 to 4077b01 Compare June 18, 2026 16:36
)

@model_validator(mode="after")
def validate_updates(self) -> "UpdateDatasetMetricRequest":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an inline docstring to this newly introduced validator method so all new functions are documented consistently. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The new validator method has no docstring in the final file state, and the rule requires newly added Python functions and classes to be documented inline. This is a real rule violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 643:643
**Comment:**
	*Custom Rule: Add an inline docstring to this newly introduced validator method so all new functions are documented consistently.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

class DatasetMetricDetail(SqlMetricInfo):
"""Full saved-metric details, including identifiers."""

id: int | None = Field(None, description="Metric ID")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Do not expose the internal integer metric ID in this new response model when a UUID field is already provided; return only the UUID public identifier. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The new public response model DatasetMetricDetail exposes an internal integer id alongside a uuid field. This matches the rule about not exposing internal integer IDs when a UUID public identifier is being added or changed.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 660:660
**Comment:**
	*Custom Rule: Do not expose the internal integer metric ID in this new response model when a UUID field is already provided; return only the UUID public identifier.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +73 to +74
def _serialize_metric(metric: Any) -> DatasetMetricDetail:
currency = getattr(metric, "currency", None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an inline docstring to this new helper function describing what fields are serialized and the expected output object. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is another newly added Python helper function without a docstring. Since the rule requires new functions to be documented inline, this is a real violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/tool/update_dataset_metric.py
**Line:** 73:74
**Comment:**
	*Custom Rule: Add an inline docstring to this new helper function describing what fields are serialized and the expected output object.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +38 to +40
@pytest.fixture
def mcp_server():
return mcp

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a short docstring to the mcp_server fixture describing what object it provides to tests. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

The mcp_server fixture is a newly added function and it does not include a docstring. The custom rule requires newly added Python functions and classes to be documented inline, so this is a real violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 38:40
**Comment:**
	*Custom Rule: Add a short docstring to the `mcp_server` fixture describing what object it provides to tests.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +43 to +44
@pytest.fixture(autouse=True)
def mock_auth():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add explicit type hints to the mock_auth fixture signature, including an appropriate return type for the yielded mock object. [custom_rule]

Severity Level: Minor ⚠️

Why it matters? 🤔

This is a newly added Python fixture in a new source file, so the custom rule requires it to be fully typed. The fixture signature omits a return type annotation, and the yielded mock object is not typed either, so the suggestion identifies a real violation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 43:44
**Comment:**
	*Custom Rule: Add explicit type hints to the `mock_auth` fixture signature, including an appropriate return type for the yielded mock object.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +557 to +667
UPDATABLE_METRIC_FIELDS: frozenset[str] = frozenset(
{
"metric_name",
"expression",
"verbose_name",
"description",
"d3format",
"metric_type",
"currency",
"warning_text",
"extra",
}
)


class MetricCurrency(BaseModel):
"""Currency formatting configuration for a metric."""

symbol: str | None = Field(
None, description="ISO 4217 currency code (e.g. 'USD', 'EUR')."
)
symbolPosition: Literal["prefix", "suffix"] | None = Field( # noqa: N815
None, description="Where to render the symbol relative to the value."
)


class UpdateDatasetMetricRequest(BaseModel):
"""Request schema for update_dataset_metric."""

model_config = ConfigDict(populate_by_name=True)

dataset_id: int | str = Field(
...,
description="Dataset identifier — numeric ID or UUID string. "
"Use list_datasets to find valid IDs.",
)
metric: int | str = Field(
...,
description="Metric to update — numeric metric ID, metric UUID, or "
"metric_name (e.g. 'sum_revenue'). Numeric strings are treated as IDs. "
"Use get_dataset_info to discover a dataset's saved metrics.",
)
metric_name: str | None = Field(
None,
max_length=255,
description="New metric name, unique within the dataset. "
"Only pass this to rename the metric.",
)
expression: str | None = Field(
None,
description="New SQL aggregation expression (e.g. 'SUM(revenue)').",
)
verbose_name: str | None = Field(
None, max_length=1024, description="Human-friendly display label."
)
description: str | None = Field(None, description="Metric description.")
d3format: str | None = Field(
None,
max_length=128,
description="D3 number format string (e.g. ',.2f', '.1%').",
)
metric_type: str | None = Field(
None, max_length=32, description="Metric type (e.g. 'count', 'sum')."
)
currency: MetricCurrency | None = Field(
None, description="Currency formatting configuration."
)
warning_text: str | None = Field(
None, description="Warning shown to users of this metric."
)
extra: str | None = Field(
None, description="JSON-encoded string with extra metric metadata."
)

def updates(self) -> Dict[str, Any]:
"""Return only the metric properties explicitly provided by the caller.

``exclude_unset`` distinguishes "not provided" (leave the stored value
alone) from an explicit ``null`` (clear the stored value).
"""
return self.model_dump(
exclude_unset=True,
include=set(UPDATABLE_METRIC_FIELDS),
)

@model_validator(mode="after")
def validate_updates(self) -> "UpdateDatasetMetricRequest":
provided = self.model_fields_set & UPDATABLE_METRIC_FIELDS
if not provided:
raise ValueError(
"At least one metric property must be provided to update. "
f"Updatable properties: {sorted(UPDATABLE_METRIC_FIELDS)}."
)
if "metric_name" in provided and not (self.metric_name or "").strip():
raise ValueError("metric_name cannot be empty or null")
if "expression" in provided and not (self.expression or "").strip():
raise ValueError("expression cannot be empty or null")
return self


class DatasetMetricDetail(SqlMetricInfo):
"""Full saved-metric details, including identifiers."""

id: int | None = Field(None, description="Metric ID")
uuid: str | None = Field(None, description="Metric UUID")
metric_type: str | None = Field(None, description="Metric type")
currency: MetricCurrency | None = Field(
None, description="Currency formatting configuration"
)
warning_text: str | None = Field(None, description="Warning text")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: extra is declared as an updatable metric property, but the response model for the updated metric omits that field. This creates an API contract gap where updates to extra succeed but are not returned in metric, so callers cannot verify the applied value and may treat the update as failed or stale. Add extra to DatasetMetricDetail (and keep serialization aligned) so response payloads reflect all supported updates. [incomplete implementation]

Severity Level: Major ⚠️
- ⚠️ MCP update_dataset_metric hides updated metric extra metadata.
- ⚠️ Callers cannot verify extra value via MCP response.
- ⚠️ Request/response contract inconsistent for extra field updates.
Steps of Reproduction ✅
1. Start the MCP server (`mcp` fixture from `superset.mcp_service.app`, imported in
`tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py:25`) and connect
a FastMCP client as done in `test_update_dataset_metric_success` at lines 139–184 (the
client calls the `"update_dataset_metric"` tool via `Client(mcp_server).call_tool(...)`).

2. From the client, invoke the `update_dataset_metric` tool (entry point
`superset/mcp_service/dataset/tool/update_dataset_metric.py:113`) with a request body that
includes `extra`, for example:

   `{"request": {"dataset_id": 1, "metric": "count", "extra": "{\"foo\": \"bar\"}"}}`.

   This request is validated by `UpdateDatasetMetricRequest` in
   `superset/mcp_service/dataset/schemas.py:64-110`, where `extra` is an allowed,
   updatable field (`UPDATABLE_METRIC_FIELDS` includes `"extra"` at lines 38–50 in the
   same file, corresponding to diff lines 557–569).

3. In `update_dataset_metric`, the tool computes `updates = request.updates()` at
`update_dataset_metric.py:142`, which includes `"extra": "<value>"` because
`UPDATABLE_METRIC_FIELDS` contains `"extra"` and `updates()` explicitly includes all
provided updatable fields (lines 112–121 of `schemas.py`). The tool then persists these
updates via `UpdateDatasetCommand(...).run()` at `update_dataset_metric.py:207–209`, so
the underlying metric's `extra` is updated successfully.

4. After the update, the tool builds the response metric via
`_serialize_metric(updated_metric)` at `update_dataset_metric.py:220–223`.
`_serialize_metric` (lines 73–100) constructs a `DatasetMetricDetail` instance, but only
passes `id`, `uuid`, `metric_name`, `verbose_name`, `expression`, `description`,
`d3format`, `metric_type`, `currency`, and `warning_text`; it never includes `extra`.
`DatasetMetricDetail` itself (defined in
`superset/mcp_service/dataset/schemas.py:138–147`, diff lines 657–667) extends
`SqlMetricInfo` (lines 120–129) and adds `metric_type`, `currency`, and `warning_text`,
but does not declare an `extra` field. As a result, when the MCP client deserializes the
tool result exactly as in `test_update_dataset_metric_success` (lines 173–186, using
`json.loads(result.content[0].text)`), the top-level response contains
`updated_properties` including `"extra"` (because `updates` included it), but
`data["metric"]` has no `extra` key or value. Callers using only MCP APIs can see that
`extra` was updated but cannot see the applied `extra` content, creating an inconsistent
API contract between what can be written and what is returned.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/schemas.py
**Line:** 557:667
**Comment:**
	*Incomplete Implementation: `extra` is declared as an updatable metric property, but the response model for the updated metric omits that field. This creates an API contract gap where updates to `extra` succeed but are not returned in `metric`, so callers cannot verify the applied value and may treat the update as failed or stale. Add `extra` to `DatasetMetricDetail` (and keep serialization aligned) so response payloads reflect all supported updates.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +62 to +70
def _metric_not_found_message(metrics: list[Any], identifier: int | str) -> str:
names = [m.metric_name for m in metrics]
msg = f"Metric '{identifier}' not found on this dataset."
if not names:
return f"{msg} This dataset has no saved metrics."
suggestions = difflib.get_close_matches(str(identifier), names, n=3, cutoff=0.6)
if suggestions:
return f"{msg} Did you mean: {', '.join(suggestions)}?"
return f"{msg} Available metrics: {', '.join(sorted(names))}."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The not-found error message interpolates raw user-controlled identifier and stored metric names directly into the response text without LLM-context sanitization, which can inject delimiter/control content into MCP outputs. Sanitize or escape both identifier and suggested/available metric names before constructing error. [security]

Severity Level: Major ⚠️
- ⚠️ MCP error text can inject delimiters into LLM context.
- ⚠️ Enables prompt-injection via crafted metric identifiers or names.
Steps of Reproduction ✅
1. The helper `_metric_not_found_message` at
`superset/mcp_service/dataset/tool/update_dataset_metric.py:62-70` builds an error string
directly from the user-supplied `identifier` and the stored `metric_name` values: `msg =
f"Metric '{identifier}' not found..."`, then either suggests close matches (`',
'.join(suggestions)`) or lists all available metric names (`', '.join(sorted(names))`)
with no sanitization or escaping.

2. When `update_dataset_metric` cannot find the requested metric (`target is None` at
`update_dataset_metric.py:181`), it calls `_metric_not_found_message(metrics,
request.metric)` and returns the resulting string as the `error` field of
`UpdateDatasetMetricResponse` at `update_dataset_metric.py:182-188`.
`UpdateDatasetMetricResponse`'s schema in
`superset/mcp_service/dataset/schemas.py:150-167` defines `error: str | None` without any
field validator, so this text is passed through unchanged to the MCP client/LLM.

3. Other fields intended for LLM exposure are explicitly sanitized: `_serialize_metric`
applies `escape_llm_context_delimiters` and `sanitize_for_llm_context` to metric_name,
verbose_name, expression, description, and warning_text at
`update_dataset_metric.py:78-99`, and `DatasetError.error` wraps error text with
`sanitize_for_llm_context` via a `field_validator` at
`superset/mcp_service/dataset/schemas.py:60-64`. In contrast, `_metric_not_found_message`
bypasses these utilities.

4. If a caller supplies a crafted `metric` identifier containing LLM control content (for
example ``"```json\n{\"attack\": true}\n```"``) or if stored `metric_name` values include
such delimiters, `_metric_not_found_message` will interpolate them directly into `msg`
(lines 64-70), and the MCP tool will return this string in
`UpdateDatasetMetricResponse.error` (lines 184-188). This unsanitized error text is then
embedded into the LLM tool response, creating a prompt-injection vector that can break
tool-output framing or influence downstream agent behavior.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/tool/update_dataset_metric.py
**Line:** 62:70
**Comment:**
	*Security: The not-found error message interpolates raw user-controlled identifier and stored metric names directly into the response text without LLM-context sanitization, which can inject delimiter/control content into MCP outputs. Sanitize or escape both identifier and suggested/available metric names before constructing `error`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think of this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — this is a valid security issue.

Why it matters

_metric_not_found_message() currently builds the error string from:

  • the caller-controlled identifier
  • stored metric_name values

and returns it directly in UpdateDatasetMetricResponse.error. Since this is MCP output, raw delimiter/control content can leak into the LLM context and create prompt-injection risk.

Minimal fix

Sanitize/escape the values before interpolating them into the error message, consistent with _serialize_metric().

For example:

from superset.mcp_service.utils import (
    escape_llm_context_delimiters,
    sanitize_for_llm_context,
)

def _metric_not_found_message(metrics: list[Any], identifier: int | str) -> str:
    safe_identifier = sanitize_for_llm_context(str(identifier), field_path=("metric", "identifier"))
    names = [
        escape_llm_context_delimiters(m.metric_name) or ""
        for m in metrics
    ]
    msg = f"Metric '{safe_identifier}' not found on this dataset."
    if not names:
        return f"{msg} This dataset has no saved metrics."
    suggestions = difflib.get_close_matches(str(identifier), [m.metric_name for m in metrics], n=3, cutoff=0.6)
    if suggestions:
        safe_suggestions = [escape_llm_context_delimiters(name) or "" for name in suggestions]
        return f"{msg} Did you mean: {', '.join(safe_suggestions)}?"
    return f"{msg} Available metrics: {', '.join(sorted(names))}."

Recommendation

I’d keep the fix focused on this helper rather than changing the response schema, since the issue is specifically in how the message is composed.

If you want, I can also check the rest of the PR comments and help validate/fix those too.

Comment on lines +179 to +188
metrics = list(dataset.metrics)
target = _find_metric(metrics, request.metric)
if target is None:
message = _metric_not_found_message(metrics, request.metric)
await ctx.warning("Metric not found: %s" % (request.metric,))
return UpdateDatasetMetricResponse(
dataset_id=dataset.id,
dataset_name=dataset.table_name,
error=message,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This code performs metric existence checks before ownership is enforced, so a caller who can invoke the tool but does not own the dataset can distinguish "metric exists" vs "metric not found" and enumerate metric names via error messages/suggestions. Enforce ownership immediately after dataset resolution (before _find_metric / _metric_not_found_message) so unauthorized callers always get the same forbidden response. [security]

Severity Level: Major ⚠️
- ⚠️ MCP `update_dataset_metric` leaks metric existence to non-owners.
- ⚠️ Enables metric name enumeration via distinct error responses.
Steps of Reproduction ✅
1. In `superset/mcp_service/dataset/tool/update_dataset_metric.py:103-113`, the
`update_dataset_metric` FastMCP tool is exposed with `class_permission_name="Dataset"` and
`method_permission_name="write"`, meaning any role with the Dataset write class permission
can invoke this tool from an MCP client.

2. When the tool runs, it first resolves the dataset via
`resolve_dataset(request.dataset_id, eager_options)` at
`update_dataset_metric.py:167-168`, which calls `DatasetDAO.find_by_id` through
`resolve_dataset` (`superset/mcp_service/dataset/dataset_utils.py:25-48`). This applies
the `DatasourceFilter` base filter (`superset/daos/dataset.py:52-61`) for data-access
permissions but does not enforce ownership.

3. After resolution, before any ownership check, the tool materializes all metrics and
attempts to locate the target metric (`metrics = list(dataset.metrics)` and `target =
_find_metric(...)` at `update_dataset_metric.py:179-180`). If the metric is not found,
`_metric_not_found_message` is called and a detailed error is returned at
`update_dataset_metric.py:181-188`, including the dataset ID/name (from `dataset.id` /
`dataset.table_name`) and suggested/available metric names.

4. Only when the metric *is* found does execution continue to the update path, where
`UpdateDatasetCommand(...).run()` is invoked at `update_dataset_metric.py:207-209`. Inside
`UpdateDatasetCommand.validate` (`superset/commands/dataset/update.py:98-112`), ownership
is enforced via `security_manager.raise_for_ownership`, which can raise
`DatasetForbiddenError` and is caught at `update_dataset_metric.py:236-243` to return the
generic `"You must be an owner..."` error. A caller who can invoke the tool but is not an
owner can therefore distinguish (a) invalid dataset ID (`dataset is None` at lines
170-177), (b) non-existent metric (`target is None` at lines 179-188 with a
metric-not-found message listing metrics), and (c) existing metric but forbidden update
(DatasetForbiddenError at lines 236-243), allowing probing and enumeration of metric
existence and names on datasets they cannot edit.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/tool/update_dataset_metric.py
**Line:** 179:188
**Comment:**
	*Security: This code performs metric existence checks before ownership is enforced, so a caller who can invoke the tool but does not own the dataset can distinguish "metric exists" vs "metric not found" and enumerate metric names via error messages/suggestions. Enforce ownership immediately after dataset resolution (before `_find_metric` / `_metric_not_found_message`) so unauthorized callers always get the same forbidden response.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THis seems relevant

@bito-code-review

bito-code-review Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #b96f16

Actionable Suggestions - 0
Additional Suggestions - 4
  • superset/mcp_service/dataset/tool/update_dataset_metric.py - 2
    • Inconsistent sanitization on metric_name · Line 78-78
      Other user-facing metric fields (expression, description, verbose_name) use `sanitize_for_llm_context` which wraps strings in delimiters. Using escape-only for `metric_name` is inconsistent and leaves it without LLM-context protection.
    • Incorrect destructive annotation · Line 110-110
      This tool modifies persisted metric properties that affect all charts using the metric — a semantically destructive change that should be flagged as such. Update chart operations use `destructiveHint=True` for comparable data modifications.
  • tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py - 1
    • Weak error assertion in test · Line 378-378
      The assertion `assert data["error"] is not None` is weaker than sibling error tests in this file which check specific error substrings (e.g., `"No dataset found" in data["error"]` at line 301, `"owner" in data["error"]` at line 336). Consider asserting that the error message contains expected validation content, such as `"metric_name"` or a duplicate-related keyword, to ensure the specific `DatasetMetricsDuplicateValidationError` is properly surfaced through the error handling path.
  • superset/mcp_service/app.py - 1
    • Incomplete tool capability list · Line 167-167
      The description "expression, name, label, format" omits six other supported fields (description, metric_type, currency, warning_text, extra, verbose_name as distinct from name). Agents using the tool list may miss these capabilities.
Review Details
  • Files reviewed - 8 · Commit Range: 4077b01..4077b01
    • superset/mcp_service/app.py
    • superset/mcp_service/dataset/dataset_utils.py
    • superset/mcp_service/dataset/schemas.py
    • superset/mcp_service/dataset/tool/__init__.py
    • superset/mcp_service/dataset/tool/query_dataset.py
    • superset/mcp_service/dataset/tool/update_dataset_metric.py
    • tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py
    • tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@EnxDev EnxDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnxDev's Review Agent — #40975

Overall

Well-scoped addition. I verified the two key assumptions:

DatasetDAO.update_metrics() merges metric stubs over the stored metrics, so rebuilding the full list preserves untouched metrics without creating or deleting entries.
UpdateDatasetCommand enforces ownership and validates metric expressions before persisting changes.

🟡 Should fix

  • update_dataset_metric.py:62-70 – _metric_not_found_message returns metric names and suggestions without applying the same sanitization used in _serialize_metric. It would be safer to escape these values as well to keep the error path consistent with the success path. A regression test would help prevent regressions.

  • update_dataset_metric.py:108-110 – Consider destructiveHint=True. Updating a metric changes all dependent charts and overwrites the previous definition, which seems closer to a destructive operation than a purely additive one.

🔵 Nits
Add min_length=1 for d3format and metric_type to match the existing Marshmallow schema.
Mention in the docs that purely numeric metric names must be referenced by ID/UUID.
Optionally validate that extra contains valid JSON instead of silently accepting invalid values.

🙌 Nice work
Routing updates through UpdateDatasetCommand correctly reuses existing authorization and validation logic.
Rebuilding the metric list as stubs is a clean way to avoid accidental deletions.
exclude_unset provides a nice implementation for partial updates.

Summary

Looks solid overall. My only substantive request is to sanitize the error path consistently with the success path. Everything else is minor.
LGTM once the sanitization issue is addressed.

Reviewed by EnxDev's Review Agent — @EnxDev.

@alexandrusoare alexandrusoare left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall lgtm, a few nits, conflict issues solving and we are good

msyavuz added 2 commits July 13, 2026 12:08
- Escape metric names/identifier in the not-found error path so it matches
  the sanitization applied on the success path (EnxDev review)
- Mark the tool destructiveHint=True, consistent with update_chart, since it
  overwrites an existing metric and affects dependent charts
- Add docstring to _metric_not_found_message
- Validate that 'extra' is a valid JSON-encoded string; reject empty
  d3format/metric_type via min_length=1 (matches Marshmallow schema)
- Fix DEFAULT_INSTRUCTIONS to say 'verbose_name' instead of 'label'
- Add type hints to test helpers/fixtures; cover DatasetUpdateFailedError,
  error-path escaping, and extra JSON validation
@msyavuz
msyavuz force-pushed the msyavuz/feat/mcp-update-dataset-metric branch from 4077b01 to c5cf803 Compare July 13, 2026 09:22
sanitize_for_llm_context,
)

logger = logging.getLogger(__name__)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an explicit type annotation for the module logger variable to satisfy the type-hint requirement for annotatable variables. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

The new module-level variable logger is unannotated even though it can be given a concrete type such as logging.Logger. This matches the type-hint rule for annotatable variables in new Python code.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/tool/update_dataset_metric.py
**Line:** 40:40
**Comment:**
	*Custom Rule: Add an explicit type annotation for the module logger variable to satisfy the type-hint requirement for annotatable variables.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
```
"""
updates = request.updates()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a concrete type annotation for the local updates container returned from the request so this new variable is explicitly typed. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

The local variable updates is introduced without an explicit type annotation, and it is a value whose type can reasonably be annotated (for example as a mapping of updated field names to values). This is a real omission under the Python type-hint rule.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/tool/update_dataset_metric.py
**Line:** 161:161
**Comment:**
	*Custom Rule: Add a concrete type annotation for the local updates container returned from the request so this new variable is explicitly typed.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎



@pytest.mark.asyncio
async def test_update_dataset_metric_success(mcp_server) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a concrete type annotation for the test fixture argument in this async test signature. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

This new test function takes the fixture argument mcp_server without a type annotation, so it matches the custom rule requiring Python type hints on annotatable function parameters.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 173:173
**Comment:**
	*Custom Rule: Add a concrete type annotation for the test fixture argument in this async test signature.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎



@pytest.mark.asyncio
async def test_update_dataset_metric_returns_extra(mcp_server) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an explicit type hint to the fixture parameter in this test function. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

The test signature includes the fixture parameter mcp_server without any type hint, which is a real omission under the stated Python type-hint rule.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 245:245
**Comment:**
	*Custom Rule: Add an explicit type hint to the fixture parameter in this test function.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +415 to +417
async def test_update_dataset_metric_forbidden_non_owner(
mcp_server, allow_ownership
) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Provide explicit type annotations for all fixture parameters in this async test definition. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

Both fixture parameters in this new test function are unannotated, so the code does violate the type-hint requirement.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 415:417
**Comment:**
	*Custom Rule: Provide explicit type annotations for all fixture parameters in this async test definition.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎



@pytest.mark.asyncio
async def test_update_dataset_metric_invalid_error(mcp_server) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add a type annotation to the fixture argument in this test function signature. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

This test function has an untyped fixture parameter mcp_server, so the suggestion identifies a real missing type hint.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 463:463
**Comment:**
	*Custom Rule: Add a type annotation to the fixture argument in this test function signature.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎



@pytest.mark.asyncio
async def test_update_dataset_metric_update_failed(mcp_server) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Specify the expected type of the fixture parameter in this async test declaration. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

The mcp_server parameter is not type-annotated in this new test function, so it fits the custom type-hint rule violation.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
**Line:** 505:505
**Comment:**
	*Custom Rule: Specify the expected type of the fixture parameter in this async test declaration.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


with event_logger.log_context(action="mcp.query_dataset.lookup"):
dataset = _resolve_dataset(request.dataset_id, eager_options)
dataset = resolve_dataset(request.dataset_id, eager_options)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Add an explicit type annotation for the newly introduced local variable so the dataset object type is clear and compliant with the type-hinting rule. [custom_rule]

Severity Level: Minor 🧹

Why it matters? ⭐

The new local variable dataset is introduced without a type annotation, and its type is inferable/annotatable from the call site. This matches the custom rule requiring type hints on relevant Python variables that can be annotated.

Rule source 📖

.cursor/rules/dev-standard.mdc (line 28)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/mcp_service/dataset/tool/query_dataset.py
**Line:** 147:147
**Comment:**
	*Custom Rule: Add an explicit type annotation for the newly introduced local variable so the dataset object type is clear and compliant with the type-hinting rule.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

…a field

Address review feedback:
- Enforce ownership immediately after dataset resolution so unauthorized
  callers cannot enumerate metric names via the not-found error path.
- Add 'extra' to the metric detail response so all updatable properties
  are echoed back.
- Relax currency.symbolPosition to a free string to tolerate legacy
  stored values on the read-back path.
- Add docstrings to new helpers/validators and cover the
  DatasetUpdateFailedError and non-owner enumeration paths in tests.
@msyavuz
msyavuz force-pushed the msyavuz/feat/mcp-update-dataset-metric branch from c5cf803 to 02323a9 Compare July 13, 2026 10:04
@bito-code-review

bito-code-review Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #837f32

Actionable Suggestions - 0
Review Details
  • Files reviewed - 8 · Commit Range: 7bd5241..2ebaf3b
    • superset/mcp_service/app.py
    • superset/mcp_service/dataset/dataset_utils.py
    • superset/mcp_service/dataset/schemas.py
    • superset/mcp_service/dataset/tool/__init__.py
    • superset/mcp_service/dataset/tool/query_dataset.py
    • superset/mcp_service/dataset/tool/update_dataset_metric.py
    • tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py
    • tests/unit_tests/mcp_service/dataset/tool/test_update_dataset_metric.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@EnxDev EnxDev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@msyavuz
msyavuz merged commit 2fac66d into apache:master Jul 13, 2026
59 checks passed
rdg0 pushed a commit to rdg0/superset that referenced this pull request Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data:dataset Related to dataset configurations size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants