Skip to content

feat(mcp): add list_reports and get_report_info tools#40348

Open
aminghadersohi wants to merge 6 commits into
masterfrom
amin/mcp-list-reports
Open

feat(mcp): add list_reports and get_report_info tools#40348
aminghadersohi wants to merge 6 commits into
masterfrom
amin/mcp-list-reports

Conversation

@aminghadersohi
Copy link
Copy Markdown
Contributor

SUMMARY

Adds two new MCP tools for the Alerts & Reports domain:

  • list_reports: List alert and report schedules with filtering (by name, type, active, dashboard_id, chart_id), text search, sorting, select_columns, and 1-based pagination. Supports owned_by_me and created_by_me flags to filter by ownership/creator.
  • get_report_info: Fetch a single report/alert schedule by numeric ID, returning schedule configuration including type (Alert/Report), active status, cron expression, and associated dashboard or chart.

Schema discovery is wired in via get_schema(model_type="report").

Key design decisions:

  • RBAC handled automatically via ReportScheduleDAO.base_filter = ReportScheduleFilter
  • owners field stripped by USER_DIRECTORY_FIELDS privacy controls
  • No UUID support (ReportSchedule has no UUID column) — integer ID only
  • Column constants and get_report_columns() live in schema_discovery.py for DRY access across list/schema tools

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — backend-only MCP tools.

TESTING INSTRUCTIONS

  1. Start Superset with MCP service enabled
  2. Connect an MCP client and call list_reports — verify it returns reports accessible to the current user
  3. Call list_reports with {"owned_by_me": true} — verify only owned reports are returned
  4. Call get_report_info with a valid report ID — verify schedule config is returned
  5. Call get_report_info with an invalid ID — verify a not_found error is returned
  6. Call get_schema(model_type="report") — verify column/filter metadata is returned

Unit tests:

pytest tests/unit_tests/mcp_service/report/

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

aminghadersohi and others added 6 commits May 21, 2026 19:10
Adds list_reports and get_report_info MCP tools under a new
superset/mcp_service/report/ domain, following the canonical database
domain pattern. Includes unit tests and app.py registration.
- Register report model type in get_schema (Fix #1): add _get_report_schema_core
  factory + "report" entry in _SCHEMA_CORE_FACTORIES; ModelType now includes "report"
- Add OwnedByMeMixin/CreatedByMeMixin to ListReportsRequest (Fix #2)
- DRY up list_reports.py column constants (Fix #3): import REPORT_* constants and
  get_report_columns from schema_discovery; pass created_by_me/owned_by_me to run_tool
- Extend test coverage (Fix #6): humanized timestamp fields, invalid order_column
  guard, owned_by_me/created_by_me DAO filter injection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tools

- Add ReportListCore subclass in list_reports.py that overrides filter
  injection to use owners.id (instead of generic owner) and calls the
  DAO with filters= kwarg (instead of column_operators=) so tests can
  assert on the kwarg by name
- Extract _call_dao_list hook in ModelListCore so subclasses can change
  the DAO kwarg name without duplicating run_tool
- Add owners.id to SELF_REFERENCING_FILTER_COLUMNS so it is excluded
  from filters_applied in responses

Fixes: test_list_reports_owned_by_me_passed_to_dao,
       test_list_reports_created_by_me_passed_to_dao
created_by_fk was removed from SELF_REFERENCING_FILTER_COLUMNS so it
now appears in filters_applied, but ReportFilter.col Literal didn't
include it, causing pydantic validation error in list_reports responses.
…e ColumnOperator for filters_applied

- ReportFilter.col Literal no longer includes created_by_fk; callers
  must use the created_by_me flag instead (internal-only filter)
- ReportList.filters_applied typed as List[ColumnOperator] so that
  internally-injected ColumnOperator instances (e.g. created_by_fk from
  created_by_me=True) pass pydantic validation without coercion errors
@aminghadersohi aminghadersohi marked this pull request as ready for review May 22, 2026 04:07
@dosubot dosubot Bot added alert-reports Namespace | Anything related to the Alert & Reports feature api Related to the REST API labels May 22, 2026
@aminghadersohi aminghadersohi requested a review from eschutho May 22, 2026 04:08
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review Agent Run #07067a

Actionable Suggestions - 2
  • superset/mcp_service/report/schemas.py - 1
  • superset/mcp_service/report/tool/get_report_info.py - 1
Additional Suggestions - 2
  • superset/mcp_service/privacy.py - 1
    • Test gap for owners.id exclusion · Line 61-63
      The addition of `"owners.id"` to `SELF_REFERENCING_FILTER_COLUMNS` is correct and the comment on line 57 accurately documents its purpose. However, the existing schema-exclusion tests at test_get_schema.py lines 400, 430, 460 only assert that `"owner"` and `"created_by_fk_or_owner"` are absent from `filter_columns` — they do not verify `"owners.id"` is also excluded when the DAO returns it. The docstring on those tests states they exist to ensure synthetic server-generated columns are not advertised to LLM callers, which applies equally to `"owners.id"`.
  • superset/mcp_service/report/tool/list_reports.py - 1
    • Unused Serializer Parameter · Line 185-188
      The nested `_serialize_report` function ignores its `cols` parameter and unconditionally delegates to `serialize_report_object`. This is dead code in the diff's serializer path and mirrors `list_charts.py`'s identical pattern (which also ignores `cols`). Either use `serialize_report_object` directly or properly use the `cols` argument for field-level filtering.
      Code suggestion
      --- a/superset/mcp_service/report/tool/list_reports.py
      +++ b/superset/mcp_service/report/tool/list_reports.py
       @@ -182,15 +182,11 @@ async def list_reports(
            try:
                from superset.daos.report import ReportScheduleDAO
       
      -        def _serialize_report(
      -            obj: "ReportSchedule | None", cols: list[str] | None
      -        ) -> ReportInfo | None:
      -            return serialize_report_object(obj)
      -
                list_tool = ReportListCore(
                    dao_class=ReportScheduleDAO,
                    output_schema=ReportInfo,
      -            item_serializer=_serialize_report,
      +            item_serializer=serialize_report_object,
                    filter_type=ReportFilter,
                    default_columns=REPORT_DEFAULT_COLUMNS,
                    search_columns=REPORT_SEARCH_COLUMNS,
       @@ -199,6 +195,10 @@ async def list_reports(
                    all_columns=get_all_column_names(get_report_columns()),
                    sortable_columns=REPORT_SORTABLE_COLUMNS,
                    logger=logger,
      +            # Type: ignore needed because ModelListCore.item_serializer expects
      +            # Callable[[Any, list[str] | None], Any] but serialize_report_object
      +            # is Callable[[Any], ReportInfo | None]. The cols argument is handled
      +            # by model_dump context at the list level, not by individual serializers.
                )
Filtered by Review Rules

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

  • superset/mcp_service/report/tool/list_reports.py - 1
Review Details
  • Files reviewed - 14 · Commit Range: 2e87c2c..2305683
    • superset/mcp_service/app.py
    • superset/mcp_service/common/schema_discovery.py
    • superset/mcp_service/constants.py
    • superset/mcp_service/mcp_core.py
    • superset/mcp_service/privacy.py
    • superset/mcp_service/report/__init__.py
    • superset/mcp_service/report/schemas.py
    • superset/mcp_service/report/tool/__init__.py
    • superset/mcp_service/report/tool/get_report_info.py
    • superset/mcp_service/report/tool/list_reports.py
    • superset/mcp_service/system/tool/get_schema.py
    • tests/unit_tests/mcp_service/report/__init__.py
    • tests/unit_tests/mcp_service/report/tool/__init__.py
    • tests/unit_tests/mcp_service/report/tool/test_report_tools.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 +266 to +268
if dt is None:
return None
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now()
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.

datetime.now() called without timezone

Add timezone argument to datetime.now() call on line 268. Use datetime.now(timezone.utc) or datetime.now(dt.tzinfo) to ensure timezone-aware datetime.

Code suggestion
Check the AI-generated fix before applying
Suggested change
if dt is None:
return None
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now()
if dt is None:
return None
now = datetime.now(dt.tzinfo) if dt.tzinfo else datetime.now(timezone.utc)

Code Review Run #07067a


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

  • Yes, avoid them


return result

except Exception as e:
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.

Blind exception catch too broad

Replace bare Exception catch with specific exception types (e.g., ValueError, KeyError, or custom exceptions) to follow BLE001 rule and improve error handling.

Code suggestion
Check the AI-generated fix before applying
Suggested change
except Exception as e:
except (ValueError, KeyError, AttributeError) as e:

Code Review Run #07067a


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

  • Yes, avoid them

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

alert-reports Namespace | Anything related to the Alert & Reports feature api Related to the REST API size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant