Skip to content

fix(mcp): generate durable explore permalink URL instead of ephemeral form_data_key#40773

Open
aminghadersohi wants to merge 5 commits into
masterfrom
mcp-chart-explore-url
Open

fix(mcp): generate durable explore permalink URL instead of ephemeral form_data_key#40773
aminghadersohi wants to merge 5 commits into
masterfrom
mcp-chart-explore-url

Conversation

@aminghadersohi
Copy link
Copy Markdown
Contributor

SUMMARY

The generate_explore_link MCP tool was returning URLs like /explore/?form_data_key=<key> where the key is stored in Redis cache and expires in ~24h. This caused two problems for LLM-based workflows:

  1. Broken URLs after expiry: The chart config is not embedded in the URL itself, so when the key expires the URL becomes unreachable and the LLM cannot reconstruct it.
  2. LLM cannot inspect config from URL: The form_data_key is opaque — an LLM building charts iteratively cannot read back the current configuration from the URL to revise it.

This fix changes the URL strategy to use Superset's explore permalink mechanism, which stores chart state in the key-value DB table (not Redis) and does not expire. The resulting URL is /explore/p/<key>/ — bookmarkable, shareable, and reconstructible.

Fallback chain:

  1. CreateExplorePermalinkCommand (DB-backed, durable) → /explore/p/<key>/
  2. MCPCreateFormDataCommand (Redis-backed, ephemeral) on permalink failure
  3. Plain dataset URL (/explore/?datasource_type=table&datasource_id=<id>) on both failing

Response schema change: adds permalink_key (populated for durable URLs) alongside the existing form_data_key (now only populated in the Redis-fallback case).

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Before: http://host/explore/?form_data_key=L-6rhlg-CSY — expires in ~24h, config not in URL

After: http://host/explore/p/1OkEh0GK5e9lFKVMCGqEG1/ — permanent, DB-backed permalink

TESTING INSTRUCTIONS

  1. Enable MCP in Superset config
  2. Call generate_explore_link MCP tool with a dataset ID and chart config
  3. Verify the returned URL is /explore/p/<key>/ format
  4. Verify the URL remains accessible after Redis cache expiry
  5. Run unit tests: pytest tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py -x -q

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

… form_data_key

The `generate_explore_link` MCP tool previously returned a URL like
`/explore/?form_data_key=<key>` backed by Redis cache (~24h expiry).
When the key expired the URL became unreachable, preventing LLMs from
iteratively building and sharing charts.

This commit changes the URL strategy:
1. Try `CreateExplorePermalinkCommand` first (DB-backed key-value store,
   does not expire) → `/explore/p/<key>/`
2. Fall back to `MCPCreateFormDataCommand` (Redis cache) on permalink
   failure → `/explore/?form_data_key=<key>`
3. Fall back to plain dataset URL on both failing.

The tool response now includes a `permalink_key` field (populated when
the durable permalink path is used) alongside the existing `form_data_key`
(populated only in the Redis-fallback case).

`extract_permalink_key_from_url()` added to `chart_helpers.py`.
Tests updated to mock `CreateExplorePermalinkCommand.run` by default and
cover both the permalink path and each fallback tier.
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 4, 2026

Codecov Report

❌ Patch coverage is 14.28571% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 63.97%. Comparing base (0984839) to head (8b85cdb).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/chart_utils.py 0.00% 8 Missing ⚠️
superset/mcp_service/utils/url_utils.py 12.50% 7 Missing ⚠️
.../mcp_service/explore/tool/generate_explore_link.py 40.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #40773      +/-   ##
==========================================
- Coverage   64.19%   63.97%   -0.22%     
==========================================
  Files        2666     2648      -18     
  Lines      143991   143092     -899     
  Branches    33108    32947     -161     
==========================================
- Hits        92428    91543     -885     
+ Misses      49950    49940      -10     
+ Partials     1613     1609       -4     
Flag Coverage Δ
hive 39.51% <14.28%> (-0.26%) ⬇️
mysql 58.25% <14.28%> (-0.16%) ⬇️
postgres 58.32% <14.28%> (-0.16%) ⬇️
presto 41.11% <14.28%> (-0.25%) ⬇️
python 59.80% <14.28%> (-0.15%) ⬇️
sqlite 57.94% <14.28%> (-0.16%) ⬇️
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.

…urce

- Move extract_permalink_key_from_url from chart_helpers to url_utils
  and reimplement via urlparse path parsing instead of regex
- Fix generate_explore_link tool to use dataset.id (resolved numeric ID)
  instead of request.dataset_id (may be UUID string) when building the
  datasource field in the returned form_data dict
- Add unit tests for extract_permalink_key_from_url in test_url_utils.py
@aminghadersohi aminghadersohi marked this pull request as ready for review June 4, 2026 18:45
@dosubot dosubot Bot added the explore Namespace | Anything related to Explore label Jun 4, 2026
@aminghadersohi aminghadersohi requested a review from Copilot June 4, 2026 18:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@aminghadersohi aminghadersohi requested a review from rusackas June 4, 2026 18:50
Copilot stopped reviewing on behalf of aminghadersohi due to an error June 4, 2026 19:45
@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Jun 4, 2026

Code Review Agent Run #d40bd6

Actionable Suggestions - 0
Review Details
  • Files reviewed - 5 · Commit Range: 62fae8c..d694de8
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/explore/tool/generate_explore_link.py
    • superset/mcp_service/utils/url_utils.py
    • tests/unit_tests/mcp_service/explore/tool/test_generate_explore_link.py
    • tests/unit_tests/mcp_service/test_url_utils.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

Copy link
Copy Markdown
Member

@rusackas rusackas left a comment

Choose a reason for hiding this comment

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

LGTM, but the permalink-creation except is very broad (TypeError/AttributeError/KeyError/ValueError alongside the expected ExplorePermalinkCreateFailedError/SQLAlchemyError) — it could mask a genuine bug behind a silent fallback, but it's a deliberate resilience path with debug logging.

…modes

Address review feedback that the permalink-creation except was too broad.
CreateExplorePermalinkCommand already wraps its internal failures into
ExplorePermalinkCreateFailedError, so catch only that and SQLAlchemyError
in the permalink and outer fallback blocks. Programming errors (TypeError,
AttributeError, KeyError, ValueError) now surface to the tool handler
instead of being silently masked behind a fallback URL.
@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Jun 6, 2026

Code Review Agent Run #648f3b

Actionable Suggestions - 0
Review Details
  • Files reviewed - 1 · Commit Range: d694de8..c8d6f09
    • superset/mcp_service/chart/chart_utils.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

After narrowing the explore-link exception handling, the durable-permalink
path no longer swallows the AttributeError raised by check_chart_access
accessing g.user outside a request context. Patch CreateExplorePermalinkCommand.run
to fail explicitly so the test deterministically exercises the form_data_key
fallback, matching the test's stated intent.
@netlify
Copy link
Copy Markdown

netlify Bot commented Jun 6, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 8b85cdb
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a246defec2dbb00087194ac
😎 Deploy Preview https://deploy-preview-40773--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 +217 to +219
state = {"formData": form_data_with_datasource}
permalink_key = CreateExplorePermalinkCommand(state=state).run()
return f"{base_url}/explore/p/{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: Returning a permalink URL by default breaks existing callers that still rely on a form_data_key URL contract. update_chart_preview immediately parses the generated URL with extract_form_data_key_from_url and now fails with "missing form_data_key" for the normal success path, and generate_chart preview mode similarly loses its unsaved-state key. Keep permalink generation for the explore tool path, but preserve/optionally force form_data_key generation for shared callers that require it (or update those callers to consume permalink keys end-to-end before changing this shared helper default). [api mismatch]

Severity Level: Critical 🚨
- ❌ update_chart_preview tool returns PreviewError for valid preview updates.
- ⚠️ generate_chart preview responses no longer expose form_data_key.
- ⚠️ LLM workflows cannot reuse cached preview state keys.
Steps of Reproduction ✅
1. Invoke the MCP `generate_chart` tool in preview mode by calling
`generate_chart(request={... save_chart: false ...})` as documented in
`superset/mcp_service/app.py:102-114` and implemented in
`superset/mcp_service/chart/tool/generate_chart.py:81-161`.

2. In the preview-only branch of `generate_chart` at
`superset/mcp_service/chart/tool/generate_chart.py:527-535`, observe that it imports
`generate_explore_link` and calls `explore_url = generate_explore_link(request.dataset_id,
form_data)` (line 530), then immediately calls `form_data_key =
extract_form_data_key_from_url(explore_url)` (lines 533-534) to populate the response's
`form_data_key`.

3. The shared helper `generate_explore_link` used above is implemented in
`superset/mcp_service/chart/chart_utils.py:160-116`; on the normal success path it now
executes `state = {"formData": form_data_with_datasource}` and `permalink_key =
CreateExplorePermalinkCommand(state=state).run()` at `chart_utils.py:217-218`, then
returns `f"{base_url}/explore/p/{permalink_key}/"` at `chart_utils.py:219` — a permalink
URL with **no** `form_data_key` query parameter, so `extract_form_data_key_from_url` in
`superset/mcp_service/chart/chart_helpers.py:19-28` returns `None`, causing
`generate_chart` preview responses to lose the previously-populated unsaved preview
`form_data_key`.

4. Independently, call the MCP `update_chart_preview` tool defined at
`superset/mcp_service/chart/tool/update_chart_preview.py:115-33` with a valid `dataset_id`
and `config`. After validation, the function generates a new explore URL via `explore_url
= generate_explore_link(request.dataset_id, new_form_data)` at
`update_chart_preview.py:244`, then immediately runs `new_form_data_key =
extract_form_data_key_from_url(explore_url)` at `update_chart_preview.py:247` and checks
`if not new_form_data_key:` returning an error payload with `error_type: "PreviewError"`
and message `"Failed to generate preview: missing form_data_key"` at
`update_chart_preview.py:248-156`. Because the shared `generate_explore_link` now returns
a `/explore/p/<permalink_key>/` URL without a `form_data_key` under normal conditions,
this error path is taken even when permalink generation succeeds, breaking the normal
success flow for `update_chart_preview` and any clients that follow the preview-first
workflow described in `superset/mcp_service/app.py:102-114`.

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/chart/chart_utils.py
**Line:** 217:219
**Comment:**
	*Api Mismatch: Returning a permalink URL by default breaks existing callers that still rely on a `form_data_key` URL contract. `update_chart_preview` immediately parses the generated URL with `extract_form_data_key_from_url` and now fails with `"missing form_data_key"` for the normal success path, and `generate_chart` preview mode similarly loses its unsaved-state key. Keep permalink generation for the explore tool path, but preserve/optionally force `form_data_key` generation for shared callers that require it (or update those callers to consume permalink keys end-to-end before changing this shared helper default).

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 generate_explore_link function now defaults to returning a permalink URL, which lacks the form_data_key query parameter expected by existing callers like update_chart_preview and generate_chart. This causes those callers to fail when they attempt to extract the key.

To resolve this, you can update generate_explore_link to accept an optional force_form_data_key parameter. When set to True, the function will skip the permalink generation and proceed directly to the form_data_key path, preserving the expected contract for legacy callers.

Would you like me to implement this change and check the rest of the comments on this PR?

superset/mcp_service/chart/chart_utils.py

def generate_explore_link(dataset_id: int | str, form_data: Dict[str, Any], force_form_data_key: bool = False) -> str:
    # ...
    if not force_form_data_key:
        try:
            state = {"formData": form_data_with_datasource}
            permalink_key = CreateExplorePermalinkCommand(state=state).run()
            return f"{base_url}/explore/p/{permalink_key}/"
        except (ExplorePermalinkCreateFailedError, SQLAlchemyError) as permalink_e:
            # ...

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

Labels

explore Namespace | Anything related to Explore size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants