Skip to content

fix(mcp): resolve dashboard permalinks - #43482

Open
aminghadersohi wants to merge 6 commits into
apache:masterfrom
aminghadersohi:aminghadersohi/resolve-dashboard-permalinks
Open

fix(mcp): resolve dashboard permalinks#43482
aminghadersohi wants to merge 6 commits into
apache:masterfrom
aminghadersohi:aminghadersohi/resolve-dashboard-permalinks

Conversation

@aminghadersohi

Copy link
Copy Markdown
Contributor

SUMMARY

Dashboard lookup tools can resolve shared /dashboard/p/<key>/ links and bare permalink keys, returning the dashboard identifier together with the permalink's active-tab and filter state. This extends the existing tools rather than adding a resolver round trip: their response models already carry dashboard metadata and state, so transparent resolution keeps the workflow discoverable without losing context.

Invalid or expired permalinks return an actionable error asking for a fresh shared link. Numeric IDs, UUIDs, and slugs retain their existing lookup path; ambiguous bare strings are attempted as permalinks only after ordinary identifier lookup fails.

This supersedes #42659, which GitHub closed permanently when the source fork repository was deleted ("state cannot be changed. The repository that submitted this pull request has been deleted"). The fork has been recreated, but a recreated fork gets a new internal ID, so the original PR cannot be reopened. The code here is the same work, rebased onto current master. Review history from #42659 is summarized below so it is not lost.

Carried-over review history from #42659

#42659 was reviewed by @gabotorresruiz, who raised two issues and subsequently approved. Both fixes are included here:

  1. UUID dashboardIdCreateDashboardPermalinkCommand stores str(dashboard.uuid), so the original int(...) comparison failed for virtually every real permalink, dropping shared state with a spurious "belongs to a different dashboard" warning. get_matching_dashboard_permalink_state now compares the reference against id, uuid and slug (slug covers pre-3.1 permalinks), and DashboardLookupResult.resolved_from_permalink marks the permalink-only path so it skips re-verification entirely.
  2. Swallowed not-found error — a mistyped slug returned the permalink message ("ask for a fresh shared dashboard link") instead of naming the identifier. Both tools now keep the identifier's own error whenever result is not None, reserving the permalink wording for permalink-only requests.

A bot review also flagged that lookup_dashboard_reference had no direct tests despite four branching paths; nine direct tests were added covering each resolution path.

This branch was additionally rebased across #43202 ("preserve user-authored result values"), which removed sanitize_for_llm_context service-wide and rewrote _apply_permalink_state to use model_copy. Both were reconciled rather than resolved in this branch's favour: the removed sanitization call was dropped (the privacy redaction via user_can_view_data_model_metadata is retained, since that is access control rather than value rewriting) and master's model_copy form was kept.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable; this changes MCP tool behavior and schemas only.

TESTING INSTRUCTIONS

pytest -q tests/unit_tests/mcp_service/dashboard/
pre-commit run --files superset/mcp_service/app.py superset/mcp_service/dashboard/permalink.py superset/mcp_service/dashboard/schemas.py superset/mcp_service/dashboard/tool/get_dashboard_info.py superset/mcp_service/dashboard/tool/get_dashboard_layout.py tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py tests/unit_tests/mcp_service/dashboard/test_permalink.py tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py tests/unit_tests/mcp_service/dashboard/tool/test_get_dashboard_layout.py

The dashboard MCP unit suite passes (411 tests). Changed-file pre-commit passes, including mypy, ruff and pylint.

Coverage worth noting:

  • UUID-string dashboardId asserting filter_state is present, for both tools (test_dashboard_tools.py, test_get_dashboard_layout.py), plus a slug case for pre-3.1 permalinks.
  • Typo/unknown-slug tests asserting the error message names the identifier, for both tools.
  • Nine direct lookup_dashboard_reference tests. These were mutation-checked: removing the identifier-precedence guard, forcing resolved_from_permalink=False, and disabling permalink-URL extraction each cause failures, so the tests pin behaviour rather than merely passing.

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

Blast radius

Apache Superset's optional MCP service only. No migrations, feature flags, authentication changes, workspace isolation changes, or database query behavior changes.

Risk and rollback

The main risk is interpreting an unknown slug-like identifier as a permalink after normal lookup fails. Existing identifiers are resolved first, and reverting this commit restores the previous behavior.

Cost and latency delta

No model, prompt-routing, or token changes. A bare permalink key can add one permalink lookup after an unsuccessful ordinary identifier lookup; explicit permalink inputs and shared URLs resolve directly.

Prompt / non-determinism

Tool descriptions were updated to identify /dashboard/p/<key>/ links and direct agents to the lookup tools. The resolution behavior is deterministic; no model prompt or routing behavior changed.

Review guidance

Start with dashboard/permalink.py and the request/response schema changes, then review how each lookup tool reuses the resolved dashboard ID and safely exposes permalink state. The most important behavior is the ordinary-identifier-first fallback for bare strings.

aminghadersohi and others added 6 commits August 24, 2026 18:13
CreateDashboardPermalinkCommand stores dashboardId as str(dashboard.uuid),
so the int() comparison dropped shared state for virtually every real
permalink. The permalink-only path now records that it selected the
dashboard itself and skips re-verification, while the explicit
identifier + permalink_key combination compares the reference against the
dashboard id, uuid, and slug.

A plain identifier that simply does not exist also no longer reports the
permalink wording; its own not-found error is preserved and the permalink
message is reserved for requests that had to resolve through a permalink.

Co-Authored-By: Claude <noreply@anthropic.com>
Add direct unit tests for the identifier/permalink resolution branches:
identifier-only, identifier plus permalink state, unresolvable permalink,
numeric and string not-found precedence, shared URL identifiers,
permalink-only requests, bare-string fallback, and fully unresolvable
references.
@dosubot dosubot Bot added the dashboard Namespace | Anything related to the Dashboard label Aug 24, 2026
Comment on lines +302 to +303
if self.identifier is None and self.permalink_key is None:
raise ValueError("Provide identifier or permalink_key")

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: These validators only reject None, so empty strings such as {"identifier": ""} or {"permalink_key": ""} pass validation even though they represent no usable reference. The downstream resolver treats them as ordinary identifiers or falsey permalink references, producing misleading lookup/permalink errors instead of the intended “Provide identifier or permalink_key” validation error. Reject blank strings after trimming in both request validators. [incorrect condition logic]

Severity Level: Minor 🧹
- ⚠️ Blank info requests reach dashboard lookup unnecessarily.
- ⚠️ Blank layout requests return misleading permalink errors.
- ⚠️ Clients receive inconsistent validation guidance.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

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

**Path:** superset/mcp_service/dashboard/schemas.py
**Line:** 302:303
**Comment:**
	*Incorrect Condition Logic: These validators only reject `None`, so empty strings such as `{"identifier": ""}` or `{"permalink_key": ""}` pass validation even though they represent no usable reference. The downstream resolver treats them as ordinary identifiers or falsey permalink references, producing misleading lookup/permalink errors instead of the intended “Provide identifier or permalink_key” validation error. Reject blank strings after trimming in both request validators.

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 +115 to +117
permalink_state = get_matching_dashboard_permalink_state(
lookup_result, result.id, result.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: Legacy permalinks can store the dashboard slug in dashboardId, but this call verifies the permalink using only result.id and result.uuid. Because DashboardLayout does not expose the slug, a valid slug-based permalink is treated as belonging to a different dashboard and its active-tab/filter state is discarded, unlike get_dashboard_info. Include the dashboard slug in the layout lookup/serialization or otherwise pass it to the matching check. [api mismatch]

Severity Level: Major ⚠️
- ⚠️ Layout permalink state is discarded for legacy slug references.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

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

**Path:** superset/mcp_service/dashboard/tool/get_dashboard_layout.py
**Line:** 115:117
**Comment:**
	*Api Mismatch: Legacy permalinks can store the dashboard slug in `dashboardId`, but this call verifies the permalink using only `result.id` and `result.uuid`. Because `DashboardLayout` does not expose the slug, a valid slug-based permalink is treated as belonging to a different dashboard and its active-tab/filter state is discarded, unlike `get_dashboard_info`. Include the dashboard slug in the layout lookup/serialization or otherwise pass it to the matching check.

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

Copy link
Copy Markdown
Contributor

The flagged issue is correct. The current model_validator only checks if identifier and permalink_key are None, allowing empty strings to pass, which causes downstream resolution errors. To resolve this, you should update the validators to reject blank strings after trimming.

Here is the corrected implementation for superset/mcp_service/dashboard/schemas.py:

    @model_validator(mode="after")
    def _require_identifier_or_permalink(self) -> "GetDashboardInfoRequest":
        if (self.identifier is None or (isinstance(self.identifier, str) and not self.identifier.strip())) and \
           (self.permalink_key is None or not self.permalink_key.strip()):
            raise ValueError("Provide identifier or permalink_key")
        return self

(Apply this same logic to GetDashboardLayoutRequest as well.)

Would you like me to fetch all other comments on this PR to validate and implement fixes for them as well?

superset/mcp_service/dashboard/schemas.py

@model_validator(mode="after")
    def _require_identifier_or_permalink(self) -> "GetDashboardInfoRequest":
        if (self.identifier is None or (isinstance(self.identifier, str) and not self.identifier.strip())) and \
           (self.permalink_key is None or not self.permalink_key.strip()):
            raise ValueError("Provide identifier or permalink_key")
        return self

@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 #af015b

Actionable Suggestions - 1
  • superset/mcp_service/dashboard/permalink.py - 1
Filtered by Review Rules

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

  • superset/mcp_service/dashboard/schemas.py - 2
Review Details
  • Files reviewed - 9 · Commit Range: 1f6f618..8da8807
    • superset/mcp_service/app.py
    • superset/mcp_service/dashboard/permalink.py
    • superset/mcp_service/dashboard/schemas.py
    • superset/mcp_service/dashboard/tool/get_dashboard_info.py
    • superset/mcp_service/dashboard/tool/get_dashboard_layout.py
    • tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py
    • tests/unit_tests/mcp_service/dashboard/test_permalink.py
    • tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_tools.py
    • tests/unit_tests/mcp_service/dashboard/tool/test_get_dashboard_layout.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 an incremental AI Review.

  • /review full - 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


raw_state = value.get("state")
state: dict[str, object] = dict(raw_state) if isinstance(raw_state, dict) else {}
if not user_can_view_data_model_metadata():

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 test for privacy redaction

The privacy-gated redaction on line 182 is not exercised by existing tests. test_get_matching_dashboard_permalink_state_skips_check_when_permalink_resolved passes resolved_from_permalink=True and hits the early-return before line 182, while the other get_matching_dashboard_permalink_state tests never call user_can_view_data_model_metadata or redact_filter_state_data_model_metadata. Add coverage to prevent silent regressions if the redaction logic is ever refactored.

Code Review Run #af015b


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

  • Yes, avoid them

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.12403% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.94%. Comparing base (c980b3a) to head (8da8807).

Files with missing lines Patch % Lines
superset/mcp_service/dashboard/permalink.py 94.38% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43482      +/-   ##
==========================================
+ Coverage   78.92%   78.94%   +0.01%     
==========================================
  Files        2878     2879       +1     
  Lines      165088   165176      +88     
  Branches    38137    38153      +16     
==========================================
+ Hits       130296   130390      +94     
+ Misses      32342    32336       -6     
  Partials     2450     2450              
Flag Coverage Δ
hive 38.02% <31.78%> (+<0.01%) ⬆️
mysql 57.74% <31.78%> (-0.03%) ⬇️
postgres 57.78% <31.78%> (-0.02%) ⬇️
presto 39.94% <31.78%> (-0.01%) ⬇️
python 83.63% <96.12%> (+0.02%) ⬆️
sqlite 57.47% <31.78%> (-0.03%) ⬇️
unit 73.72% <96.12%> (+0.05%) ⬆️

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.

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

Thanks Amin for carefully reconstructing this after the fork mishap, and for the thorough carried-over summary in the description.

I verified the recreation rather than assuming it: all five commits from the approved #42659 are patch-identical here (git range-diff shows them equal), and the source files are byte-identical to the head I approved there, so the live verification from that review carries over: a permalink created through the real dashboard permalink API round-trips its uuid dashboardId and full filter state through both MCP tools over HTTP, and a mistyped identifier reports its own not-found error. The only new content is the test-only commit adding the nine direct lookup_dashboard_reference tests, which read well and cover each resolution branch. Locally the dashboard suite passes (411 tests) and the full mcp_service suite passes (3579 tests) at this head, and CI is green.

The bot note about empty-string identifiers is cosmetic: I checked that {"identifier": ""} returns a clean not_found naming the identifier without touching the permalink store, so tightening the validator is optional. Same for the layout slug note, which we already agreed was a non-blocking follow-up.

LGTM.

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

Labels

dashboard Namespace | Anything related to the Dashboard size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants