Skip to content

feat(mcp): add query_dataset tool to query datasets using semantic layer - #39727

Merged
aminghadersohi merged 11 commits into
apache:masterfrom
aminghadersohi:aminghadersohi/query-dataset-tool
Apr 30, 2026
Merged

feat(mcp): add query_dataset tool to query datasets using semantic layer#39727
aminghadersohi merged 11 commits into
apache:masterfrom
aminghadersohi:aminghadersohi/query-dataset-tool

Conversation

@aminghadersohi

@aminghadersohi aminghadersohi commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Add a new MCP tool query_dataset that allows querying a dataset directly using its semantic layer (saved metrics, calculated columns, dimensions) without requiring a saved chart.

Problem

The MCP service currently has:

  • execute_sql — runs raw SQL (bypasses the semantic layer entirely)
  • get_chart_data — returns data from a saved chart (must create/save a chart first)
  • get_dataset_info — returns dataset metadata but not actual data

There is no way for an MCP client to query a dataset's curated metrics and dimensions ad-hoc.

Solution

New query_dataset tool that accepts dataset_id + metrics / columns / filters / time_range and returns tabular data by leveraging the existing QueryContextFactory + ChartDataCommand pipeline.

Changes

  • New tool: superset/mcp_service/dataset/tool/query_dataset.py
  • New schemas: QueryDatasetFilter, QueryDatasetRequest, QueryDatasetResponse in dataset/schemas.py
  • Registration: Tool registered in app.py with instructions and workflow documentation
  • Tests: 10 unit tests covering happy path, validation, time range, filters, UUID lookup

Key design decisions

  • Reuses QueryContextFactory + ChartDataCommand — inherits RLS enforcement, dataset permissions, and caching for free
  • Pre-flight validation of column, metric, filter column, and order_by names against the dataset — returns close-match suggestions on typos instead of cryptic SQL errors
  • Filter operators validated against Superset's FilterOperator enum via Literal type
  • Time filtering via TEMPORAL_RANGE filter + granularity on the query dict (not adhoc_filters, which are a form_data concept)
  • Saved metrics as plain strings — resolved by the query engine against SqlMetric.metric_name on the dataset

Scope: Superset datasets only

This tool queries Superset's built-in semantic layer (SqlaTable datasets with saved metrics and calculated columns). It does not yet support the upcoming external semantic layer integration (SemanticView / Explorable protocol from PRs #37815#38611). When external semantic views land, MCP support for them would be a natural follow-up — the Explorable protocol that SemanticView implements is the same interface the chart data pipeline already uses, so the extension path is straightforward.

Test plan

  • 10 unit tests pass (happy path, not found, invalid metric/column with suggestions, validation errors, time range, filters, empty results, UUID lookup)
  • CI passes
  • Manual MCP client test against staging

@codecov

codecov Bot commented Apr 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 28.06122% with 141 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.36%. Comparing base (512ba43) to head (b091437).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/dataset/tool/query_dataset.py 14.28% 138 Missing ⚠️
superset/mcp_service/dataset/schemas.py 91.17% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #39727      +/-   ##
==========================================
- Coverage   64.41%   64.36%   -0.06%     
==========================================
  Files        2567     2568       +1     
  Lines      134411   134607     +196     
  Branches    31203    31227      +24     
==========================================
+ Hits        86584    86639      +55     
- Misses      46330    46471     +141     
  Partials     1497     1497              
Flag Coverage Δ
hive 39.66% <28.06%> (-0.04%) ⬇️
mysql 59.90% <28.06%> (-0.11%) ⬇️
postgres 59.99% <28.06%> (-0.11%) ⬇️
presto 41.41% <28.06%> (-0.05%) ⬇️
python 61.53% <28.06%> (-0.11%) ⬇️
sqlite 59.62% <28.06%> (-0.11%) ⬇️
unit 100.00% <ø> (ø)

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

☔ View full report in Codecov by Sentry.
📢 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.

@aminghadersohi
aminghadersohi requested a review from Copilot April 28, 2026 17:05

Copilot AI 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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a new MCP tool (query_dataset) to query Superset datasets through the existing semantic-layer-backed chart data pipeline, enabling ad-hoc metric/dimension queries without creating a chart.

Changes:

  • Introduces query_dataset MCP tool using QueryContextFactory + ChartDataCommand
  • Adds request/response/filter schemas for dataset querying
  • Registers the tool and documents intended workflow in MCP default instructions

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py Adds unit tests covering success, validation, time range, filters, ordering, and UUID lookup
superset/mcp_service/dataset/tool/query_dataset.py Implements the query_dataset tool (dataset resolution, validation, query dict creation, execution, response shaping)
superset/mcp_service/dataset/tool/init.py Exports the new tool
superset/mcp_service/dataset/schemas.py Adds schemas for request/response and filter operators
superset/mcp_service/app.py Registers the tool in default instructions and imports it into the MCP app

Comment thread superset/mcp_service/dataset/schemas.py
Comment thread superset/mcp_service/dataset/schemas.py Outdated
Comment thread superset/mcp_service/dataset/tool/query_dataset.py
Comment thread superset/mcp_service/dataset/tool/query_dataset.py Outdated
Comment thread superset/mcp_service/dataset/tool/query_dataset.py Outdated
@aminghadersohi
aminghadersohi marked this pull request as ready for review April 28, 2026 21:45
@dosubot dosubot Bot added api Related to the REST API data:dataset Related to dataset configurations labels Apr 28, 2026

@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 #79c4b0

Actionable Suggestions - 1
  • superset/mcp_service/dataset/tool/query_dataset.py - 1
Review Details
  • Files reviewed - 5 · Commit Range: 45c77f0..37ab147
    • superset/mcp_service/app.py
    • superset/mcp_service/dataset/schemas.py
    • superset/mcp_service/dataset/tool/__init__.py
    • superset/mcp_service/dataset/tool/query_dataset.py
    • tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.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 thread superset/mcp_service/dataset/tool/query_dataset.py
@aminghadersohi
aminghadersohi force-pushed the aminghadersohi/query-dataset-tool branch from 37ab147 to 78eb406 Compare April 29, 2026 21:50
@github-actions github-actions Bot removed the api Related to the REST API label Apr 29, 2026
Comment thread superset/mcp_service/dataset/schemas.py
Comment thread superset/mcp_service/dataset/schemas.py
Comment thread tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py
Comment thread tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py Outdated
Comment thread tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py
Comment thread tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py
Comment thread tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py Outdated
Comment thread superset/mcp_service/dataset/tool/query_dataset.py
Comment thread superset/mcp_service/dataset/tool/query_dataset.py
Comment thread superset/mcp_service/dataset/tool/query_dataset.py

@richardfogaca richardfogaca 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.

Posting on Richard's behalf - this is his PR reviewer agent. Forward any pushback to him and he'll loop me back in.

Left two notes below - the first one is the main functional/security thing I would look at before merge; the second is smaller hardening and coverage. All line numbers verified against HEAD 78eb406.

Functional - worth checking before merge

  • superset/mcp_service/dataset/tool/query_dataset.py:182-207

    This validation block builds the dataset column/metric sets and returns close-match suggestions before the query context reaches ChartDataCommand.validate() at line 307. That means a caller who can invoke the tool but should not inspect dataset metadata can still probe names through invalid columns, metrics, filters, or order_by values.

    WDYT - would it be worth moving the datasource/query access check ahead of metadata-dependent validation, or gating this tool with the same data-model metadata check used by get_dataset_info before returning suggestions?

Other suggestions

  • superset/mcp_service/dataset/tool/query_dataset.py:433-443

    These exception paths return the raw CommandException, SupersetException, and SQLAlchemyError text to the MCP client. Some of those messages can include rendered SQL, physical table names, engine details, or other backend context, which feels especially risky for a tool that may be exposed through general assistant clients.

    Small suggestion: could we log the full exception server-side but return a shorter sanitized client message here? Happy to keep as-is if MCP errors are intentionally treated as developer-facing diagnostics.

  • tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py:495

    The permission-denied test currently starts at ChartDataCommand.validate(), so it verifies the final query denial path but not the earlier metadata disclosure path above. A regression test for an invalid metric/column on an inaccessible dataset would make the intended boundary much clearer.

    Could we add one test that proves an access-denied dataset cannot return column/metric suggestions before validation runs?

Praise

  • superset/mcp_service/dataset/tool/query_dataset.py:298-308

    Nice direction reusing QueryContextFactory and ChartDataCommand for the actual execution path instead of building SQL directly. That keeps the new MCP surface aligned with the existing Superset query pipeline.

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Thanks for the review @richardfogaca (and to the agent posting on their behalf)! Addressed all three points in ffcd4b1:

Functional — metadata disclosure via suggestions
Added @requires_data_model_metadata_access decorator + runtime user_can_view_data_model_metadata() check (matching the get_dataset_info pattern) that fires after dataset lookup but before the validation step that calls difflib.get_close_matches. Restricted users get DataModelMetadataRestricted and never see column/metric suggestions. Added test_query_dataset_metadata_access_denied_no_suggestions to lock this boundary in.

Exception messages to client
The CommandException/SupersetException/SQLAlchemyError catches already return DatasetError with the exception string, which in practice is Superset's own error message (not raw SQL). The bare Exception catch now logs full detail server-side and returns a fixed sanitized string "An unexpected error occurred while querying the dataset." — agreed that is the higher-risk path.

Test for metadata disclosure boundary
Added the test you suggested — it patches user_can_view_data_model_metadata to return False, passes a typo metric name that would normally produce suggestions, and asserts both that the error type is DataModelMetadataRestricted and that no metric name appears in the error body.

@richardfogaca richardfogaca 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.

Posting on Richard's behalf - this is his PR reviewer agent. Forward any pushback to him and he'll loop me back in.

Thanks for the follow-up fixes. The metadata-suggestion path is much better now, but I spotted two small follow-ups from the second pass. All line numbers verified against HEAD 5d9ea45.

Functional - follow-up

  • superset/mcp_service/dataset/tool/query_dataset.py:174-189

    The new privacy gate blocks the response before the validation/suggestion path, but this ctx.info() still runs first and includes the dataset name plus column/metric counts. FastMCP sends ctx.info() messages to the connected MCP client, so a direct caller without data-model metadata access can still learn that the dataset exists and how many columns/metrics it has before receiving DataModelMetadataRestricted.

    WDYT - could we move the user_can_view_data_model_metadata() check before this dataset-specific log, or make the pre-gate log avoid the dataset name and schema counts?

  • superset/mcp_service/dataset/tool/query_dataset.py:430-443

    The new effective_filters list includes the synthesized TEMPORAL_RANGE filter, but the normal non-empty response still returns applied_filters=request.filters. That means successful time-range queries under-report the actual filters that were applied; only the empty-data branch at line 372 returns effective_filters.

    Small suggestion: could the success response use effective_filters too, and maybe add an assertion covering applied_filters for a non-empty time_range query?

Praise

  • tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py:751-791

    The new regression test is a good addition for the original suggestion-disclosure path. It directly exercises the typo case that would have returned close matches before the metadata gate.

@netlify

netlify Bot commented Apr 30, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit b091437
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/69f3b5d5df67e0000866e249
😎 Deploy Preview https://deploy-preview-39727--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.

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Thanks for the second pass @richardfogaca. Both caught — fixed in 3242c4c:

ctx.info() metadata disclosure
Moved the user_can_view_data_model_metadata() check to before the ctx.info() that logged dataset name and column/metric counts. Restricted callers now get the DataModelMetadataRestricted error before any dataset-specific info is sent to the client.

applied_filters under-report in success path
The non-empty success response was still returning request.filters — the empty-data branch was correctly fixed earlier but this path was missed. Changed to effective_filters so both response paths now include the synthesized TEMPORAL_RANGE entry when time_range is provided. Also added an assertion to test_query_dataset_with_time_range that verifies applied_filters in the response includes the TEMPORAL_RANGE filter.

@richardfogaca

richardfogaca commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Posting on Richard's behalf - this is his PR reviewer agent.

I did one more pass against HEAD 3242c4c4cd990f55041b12d7808422c5bfe873f3, specifically looking for bugs/regressions, side effects, security/privacy risk, overall design fit, DRY/SRP/KISS, and project conventions. The previous privacy/filter concerns look addressed, the tool stays aligned with the existing QueryContextFactory + ChartDataCommand path, and the focused query_dataset tests pass locally (17 passed). No new findings from me - good job tightening this up.

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread superset/mcp_service/dataset/tool/query_dataset.py
Comment thread tests/unit_tests/mcp_service/dataset/tool/test_query_dataset.py
Add a new MCP tool that allows querying a dataset directly using its
semantic layer (saved metrics, calculated columns, dimensions) without
requiring a saved chart. This addresses a customer request from Alpha
Tester Medialab who needed ad-hoc dataset queries via MCP.

The tool accepts dataset_id + metrics/columns/filters/time_range and
returns tabular data by leveraging QueryContextFactory + ChartDataCommand
internally, which ensures RLS and dataset permissions are enforced.

- New schemas: QueryDatasetFilter, QueryDatasetRequest, QueryDatasetResponse
- Pre-flight validation of column/metric names with close-match suggestions
- Time range support via TEMPORAL_RANGE filter + granularity
- 10 unit tests covering happy path, validation, time range, filters, UUID
- Validate filter ops against Superset's FilterOperator enum via Literal type
- Fix operator descriptions to match actual values (== not =, NOT IN not NOT_IN)
- Validate filter column names against dataset columns (prevents semantic layer bypass)
- Validate order_by names against columns + metrics
- Validate time_column exists on dataset when explicitly provided
- Pass cache_timeout through to QueryContextFactory (custom_cache_timeout)
- Add comment explaining hardcoded datasource_type="table" vs semantic views
- Add warning when time_column is not marked as datetime (is_dttm)
- Add test for permission denied (SupersetSecurityException)
- Add test for order_by with valid names (passes through correctly)
- Add test for order_by with invalid names (validation error)
- Add test for explicit time_column override
- Add test for non-dttm time_column warning
- Add test for invalid filter column name
- Fix filter operator in existing test ("=" -> "==" per FilterOperator enum)

Total: 17 tests (up from 10)
- Fix filter example in schema description: "=" -> "==" to match FilterOperator
- Fix unique_count to exclude nulls (None was stringified as "None")
- Cap column stats computation at 5000 rows to avoid O(rows*cols) overhead
- Compute null_count + unique_count in single pass per column
- Return DatasetError on unexpected exceptions instead of re-raising
  (consistent structured response for MCP clients)
- Add type parameters to generic types (list[Any], dict[str, Any])
- Use ErrorLevel.WARNING enum instead of bare string for SupersetError
- Add from __future__ import annotations
- Add @requires_data_model_metadata_access decorator and runtime privacy
  check before metadata-dependent validation, preventing restricted users
  from probing column/metric names via close-match suggestions
- Fix use_cache flag being silently ignored: force=not use_cache or force_refresh
- Fix applied_filters under-reporting: include synthesized TEMPORAL_RANGE
  filter in response when time_range is provided
- Add return type annotations to test fixtures and mcp_server parameters
- Add test verifying privacy gate fires before schema suggestions are returned
- Add docstrings to test helper functions
All existing tests were failing with DataModelMetadataRestricted because
the new privacy gate calls user_can_view_data_model_metadata() which
returns False in the test environment. Add it to the autouse mock_auth
fixture so tests run as a user with metadata access by default. The new
privacy-denied test explicitly overrides it to False.
…ests

The string path 'superset.mcp_service.dataset.tool.query_dataset.user_can_view_data_model_metadata'
resolves to the query_dataset *function* (shadowed by the __init__.py import)
rather than the module on some Python versions, causing AttributeError at
fixture setup. Use patch.object(query_dataset_module, ...) instead, since
query_dataset_module is already imported correctly via importlib.import_module.
- Move user_can_view_data_model_metadata() check before ctx.info() that
  reveals dataset name and column/metric counts, preventing any metadata
  disclosure to restricted callers before the privacy gate fires
- Fix applied_filters in the non-empty success response path to use
  effective_filters (includes the synthesized TEMPORAL_RANGE entry) —
  the empty-data branch was already correct but the success branch was missed
- Add assertion to test_query_dataset_with_time_range verifying that
  applied_filters in the response includes the TEMPORAL_RANGE filter
…tence side channel

Move user_can_view_data_model_metadata() check before the DAO lookup so
metadata-restricted users always receive DataModelMetadataRestricted, never
NotFound. Without this ordering, a restricted caller could probe dataset
existence by distinguishing the two different error types.

Add test_query_dataset_metadata_access_denied_nonexistent_dataset to
assert the same error is returned regardless of whether the dataset exists.
@aminghadersohi
aminghadersohi force-pushed the aminghadersohi/query-dataset-tool branch from c3d8cf3 to b091437 Compare April 30, 2026 20:04
@aminghadersohi
aminghadersohi merged commit f29d82b into apache:master Apr 30, 2026
65 checks passed
@bito-code-review

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped – PR Already Merged

Bito scheduled an automatic review for this pull request, but the review was skipped because this PR was merged before the review could be run.
No action is needed if you didn't intend to review it. To get a review, you can type /review in a comment and save it

bestlong pushed a commit to bestlong/superset that referenced this pull request May 7, 2026
qfcwell pushed a commit to qfcwell/superset that referenced this pull request May 12, 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