Skip to content

feat(mcp): add caller-scoped PNG chart previews - #44558

Merged
rusackas merged 9 commits into
apache:masterfrom
deepujain:feat/mcp-png-chart-preview-41114
Oct 20, 2026
Merged

rusackas merged 9 commits into
apache:masterfrom
deepujain:feat/mcp-png-chart-preview-41114

Conversation

@deepujain

@deepujain deepujain commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

SUMMARY

MCP clients need a chart image they can display inline. Existing preview formats provide text, data or a link. This adds format: "png" to get_chart_preview, so a client can display the image rendered by Superset. The response includes base64 PNG bytes and the image dimensions, addressing the rendered-image part of #41114. Existing formats and the ASCII default stay unchanged.

Chart and image-export checks run before the browser authenticates as the caller. Each capture owns its browser and MCP ORM session; no shared thumbnail cache or report executor is used. Guest users, unsaved chart state and extra dashboard filters return an explicit unsupported-format error for PNG. The additional metadata requested in #41114 remains separate work.

PNG previews use a separate 1,000,000-byte serialized payload limit, configurable with MCP_RESPONSE_SIZE_CONFIG["png_max_bytes"]. It counts base64 data and preview metadata. Larger images return an error asking for smaller dimensions or a URL preview; images are not silently rescaled. ASCII, table and Vega-Lite previews retain the existing max_bytes limit. As with the existing guard, this measures the payload, not the MCP transport envelope.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable to existing UI: this adds an MCP response format. A local real-Chromium fixture rendered two concurrent caller-specific SVG pages through the production machine-auth cookie path and verified distinct images and identities.

TESTING INSTRUCTIONS

With Playwright/Chromium installed on the MCP host and WEBDRIVER_BASEURL pointing to Superset, call get_chart_preview with {"identifier": <saved-chart-id>, "format": "png"}. Decode content.data as base64 PNG. Check that it renders the saved chart as the calling user, and that a user without chart access cannot capture it. Try guest auth, form_data_key, extra filters, and viewport dimensions outside 64..4096; those requests must be rejected.

Validation on the rebased head:

  • Preview, middleware and WebDriver suites: TZ=UTC .venv/bin/pytest tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py tests/unit_tests/mcp_service/test_middleware.py tests/unit_tests/utils/webdriver_test.py -q, 349 passed.
  • The tests send actual PNG bytes through a real FastMCP client: an 800-pixel image is delivered intact and a detailed 4096-pixel image is rejected. Exact-boundary tests count preview metadata, and non-PNG tests retain the existing text budget.
  • Authorization, worker cancellation, browser cleanup and ORM-session isolation tests passed. The rebase preserves the new tests on master and this PR's PNG tests; the only manual conflict resolution retained both import groups.
  • Full MCP suite on the rebased head: 8,923 passed, 12 skipped and four failures. All four failures reproduced on untouched base cb8ff2885a: three dateutil timezone-offset cases and a worker test missing the local trino dependency. These are not repaired here.
  • Pre-commit checks on all nine changed files passed, including mypy, Ruff and pylint.

Earlier validation on 5d6c555:

  • Restoring the previous size-guard bypass made five regressions fail, including all three non-PNG guard cases. The ORM regression failed against the initial implementation that copied the parent session token.
  • Real Chromium and production machine-auth cookies were exercised concurrently against a local Flask/SVG fixture. Chart lookup and permission decisions were stubbed in that fixture; it was not an end-to-end SQL/RLS dataset test.
  • Full MCP suite: 6,306 passed, four skipped, and three local-time failures; all three failing cases passed with process-local TZ=UTC. The affected time-range/query files were unchanged from the base.
  • Staged pre-commit run passed, including mypy, Ruff and pylint. Direct pylint also passed for the changed middleware/configuration files.

A live Superset browser capture with dataset permissions and RLS has not been rerun on the rebased head. Hosted CI must evaluate the new commit; the old prefixed Playwright fixture flake does not count as a pass.

Capture uses configured screenshot timeouts and a dedicated two-worker pool per MCP process. Cancelling a request does not interrupt an active capture or free its slot early; its browser closes when the capture finishes or fails. Start MCP workers after any process fork. Shutdown can wait for a running browser operation to finish or time out, as documented in the MCP README.

ADDITIONAL INFORMATION

@bito-code-review

bito-code-review Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #596c25

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/mcp_service/chart/schemas.py - 1
    • Missing field descriptions · Line 4196-4203
      `PNGPreview.width` and `PNGPreview.height` are bare `int` fields, while `data` uses `Field(..., description=...)` and sibling models (`URLPreview` at line 4219, `ASCIIPreview` at 4244, `TablePreview` at 4263) annotate every field with `Field(..., description=...)`. `superset/mcp_service/CLAUDE.md` ("Use Pydantic Schemas") shows descriptions on all fields since these schemas double as LLM-facing tool contracts; unannotated fields degrade generated tool docs.
Review Details
  • Files reviewed - 4 · Commit Range: dc69766..dc69766
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_preview.py
    • superset/utils/webdriver.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py
  • Files skipped - 1
    • superset/mcp_service/README.md - Reason: Filter setting
  • 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

@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.40260% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.84%. Comparing base (cb8ff28) to head (1478574).

Files with missing lines Patch % Lines
...perset/mcp_service/chart/tool/get_chart_preview.py 96.72% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master   #44558   +/-   ##
=======================================
  Coverage   82.83%   82.84%           
=======================================
  Files        3017     3017           
  Lines      192709   192782   +73     
  Branches    44914    44923    +9     
=======================================
+ Hits       159635   159706   +71     
- Misses      30040    30041    +1     
- Partials     3034     3035    +1     
Flag Coverage Δ
hive 35.34% <27.27%> (-0.01%) ⬇️
mysql 53.78% <27.27%> (-0.02%) ⬇️
postgres 53.79% <27.27%> (-0.02%) ⬇️
presto 37.18% <27.27%> (-0.01%) ⬇️
python 86.48% <97.40%> (+<0.01%) ⬆️
sqlite 53.53% <27.27%> (-0.02%) ⬇️
unit 80.12% <97.40%> (+0.01%) ⬆️

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.

chart = find_chart_by_identifier(chart_id)
if chart is None:
return ChartError(error="Chart not found", error_type="NotFound")
security_manager.raise_for_access(chart=chart)

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: Denied chart access raises an authorization exception inside the broad handler, so callers receive RenderError instead of the expected Forbidden response.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Api mismatch

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/chart/tool/get_chart_preview.py
**Line:** 1146:1146
**Comment:**
	*Api Mismatch: Denied chart access raises an authorization exception inside the broad handler, so callers receive `RenderError` instead of the expected `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 Author

Choose a reason for hiding this comment

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

Valid catch. Fixed in 3522c87: SupersetSecurityException from raise_for_access is now caught explicitly around the render worker and mapped to Forbidden, instead of falling into the broad except Exception that returned RenderError.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 3522c87.

Added a dedicated except SupersetSecurityException handler around the render call, returning a ChartError with error_type="Forbidden" instead of treating the denial as a rendering failure.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 1478574.

_run_png_render catches SupersetSecurityException from the render worker and returns a ChartError with error_type="Forbidden".

If that's not right, unresolve this thread and CodeAnt will leave it open.

manager._cleanup()

try:
return await asyncio.to_thread(render)

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: Cancellation cannot stop render, leaving Chromium processes and worker threads occupied until screenshot timeouts expire; repeated cancelled requests can exhaust rendering capacity.

Assessment: 🔴 Critical · 🔁 Occurrence: Rarely · 🏷️ Resource leak

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/chart/tool/get_chart_preview.py
**Line:** 1184:1184
**Comment:**
	*Resource Leak: Cancellation cannot stop `render`, leaving Chromium processes and worker threads occupied until screenshot timeouts expire; repeated cancelled requests can exhaust rendering capacity.

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 Author

Choose a reason for hiding this comment

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

Acknowledged as a known limitation. asyncio.to_thread cannot be cancelled mid-render, but occupation is bounded: the screenshot call has its own timeout and finally: manager._cleanup() always releases the browser. Killing it early would need process isolation, which is out of scope for this change.

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.

✅ Customized review instruction saved!

Instruction:

Do not flag cancellation-related resource leaks for the bounded render operation in this module when the screenshot call has its own timeout and browser resources are released in a finally cleanup block; early process isolation is out of scope.

Applied to:

  • superset/mcp_service/chart/tool/get_chart_preview.py

💡 To manage or update this instruction, visit: CodeAnt AI Settings

Comment on lines +1471 to +1475
content = (
await _generate_png_preview(chart.id, request)
if request.format == "png"
else preview_generator.generate()
)

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: PNG rejection occurs only after chart lookup and transient form-data handling, so invalid PNG requests can return NotFound or other errors instead of UnsupportedFormat.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Incomplete implementation

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/chart/tool/get_chart_preview.py
**Line:** 1471:1475
**Comment:**
	*Incomplete Implementation: PNG rejection occurs only after chart lookup and transient form-data handling, so invalid PNG requests can return `NotFound` or other errors instead of `UnsupportedFormat`.

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 Author

Choose a reason for hiding this comment

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

Checked the ordering: every PNG-unsupported precondition (unsaved chart, guest, form_data_key, extra_form_data) still resolves to UnsupportedFormat, just after lookup. A nonexistent chart returns NotFound, which is the correct contract (the resource does not exist regardless of format). No wrong error type is reachable, so leaving the ordering as is.

Comment on lines +1955 to +1957
assert isinstance(result, ChartError)
assert "secret" not in result.error
browser.assert_not_called()

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 denied case accepts any ChartError, so authorization failures converted to RenderError still pass and the test misses the required Forbidden contract.

Assessment: 🟠 Major · 🔁 Occurrence: Often · 🏷️ Api mismatch

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

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

**Path:** tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py
**Line:** 1955:1957
**Comment:**
	*Api Mismatch: The denied case accepts any `ChartError`, so authorization failures converted to `RenderError` still pass and the test misses the required `Forbidden` contract.

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 Author

Choose a reason for hiding this comment

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

Agreed. Fixed in 3522c87: the denied case now raises the real SupersetSecurityException and asserts error_type == "Forbidden", so a regression to RenderError would fail the test. The no-secret-leak assertion is kept.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 3522c87.

The denied-case test now explicitly asserts result.error_type == "Forbidden" while still verifying that sensitive error text is not exposed.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 2dde258.

The denied-case test now explicitly asserts that the returned ChartError has error_type equal to "Forbidden", while still checking that the authorization message is not exposed.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 71d8fc7.

The denied-case test now explicitly asserts that the returned ChartError has error_type equal to "Forbidden", while still checking that the authorization message is not exposed.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 3dbb1e1.

The denied-case test now explicitly asserts that the returned ChartError has error_type equal to "Forbidden", rather than accepting any ChartError.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 136e405.

The denied-case test now explicitly asserts that the returned ChartError has error_type equal to "Forbidden", rather than accepting any ChartError.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of f62453d.

The denied-case test now explicitly asserts that the returned ChartError has error_type equal to "Forbidden", rather than accepting any ChartError.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 5d6c555.

The denied-case test now explicitly asserts that the returned ChartError has error_type="Forbidden", while still verifying that the denial message is not leaked.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 1478574.

The denied-case test now explicitly asserts result.error_type == "Forbidden".

If that's not right, unresolve this thread and CodeAnt will leave it open.

deepujain added a commit to deepujain/superset that referenced this pull request Sep 23, 2026
raise_for_access raises SupersetSecurityException inside the render
worker, where the broad except Exception handler converted it to
RenderError. Catch it explicitly so denied charts return Forbidden.

Tighten test_png_preview_authorizes_before_browser: the denied case
now raises the real SupersetSecurityException and asserts the
Forbidden contract instead of accepting any ChartError.

Addresses the codeant-ai findings on apache#44558.
@netlify

netlify Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 1478574
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6ac970f77df72c000858a95c
😎 Deploy Preview https://deploy-preview-44558--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.

@bito-code-review

bito-code-review Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #5adb1e

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: dc69766..2dde258
    • superset/mcp_service/chart/tool/get_chart_preview.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.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

@deepujain

Copy link
Copy Markdown
Contributor Author

Addressed the dimension-description suggestion from Bito review #596c25: PNGPreview.width and height now describe the rendered pixel dimensions in the generated tool schema. Both fields remain required integers. The schema assertion and 89 chart-preview tests pass.

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

Hi @deepujain, thanks for this, and thanks for a PR description that actually explains the design. I went at the "caller scoped" claim hard and I could not break it, so the headline is good news. I am leaving two comments on the code. One of them I think blocks: with the shipped MCP defaults I do not believe a real chart PNG can reach the caller at all.

What I ran, on 100ee4c:

  • pytest tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py tests/unit_tests/utils/webdriver_test.py -q, 161 passed, which reproduces your number exactly. The whole tests/unit_tests/mcp_service suite is also green, 4911 passed and 2 skipped.
  • Mutation check on the Forbidden mapping you pushed: deleting the except SupersetSecurityException clause makes test_png_preview_authorizes_before_browser[denied] fail, so that fix is genuinely pinned by a test rather than passing for free.
  • Authorization differential. MachineAuthProvider.get_auth_cookies for two callers produces two different signed session cookies that decode to _user_id='11' and _user_id='29', so the browser really is the caller and not a service identity. A GuestUser returns Forbidden and never launches a browser even with EMBEDDED_SUPERSET off, where is_guest_user() is False and the guest_scope check does not fire, because GuestUser carries no id. The worker also re-resolves the chart through ChartDAO.find_by_id with ChartFilter applied, and /superset/slice/<id>/ is still @has_access protected, so there are two independent gates on the way to the pixels.
  • I checked the MCP response cache too, since a shared cache in front of a caller scoped tool would quietly undo all of the above. superset/mcp_service/caching.py refuses to enable caching unless an operator sets dangerously_share_cache_across_principals, so that is fine as shipped.
  • Response path: rendered a real chart in headless Chromium and pushed those exact bytes through a real FastMCP client with the real ResponseSizeGuardMiddleware. Numbers are in the inline comment.

CI is fully green on this head, including unit-tests (current), test-postgres (current), test-mysql, test-sqlite and pre-commit (current).

I read the CodeAnt and Bito threads first and I am not repeating any of them. Two nits I am deliberately dropping so they do not dilute the above: the form_data_key lookup still runs before PNG rejects the request, and the PNGPreview docstring describes the caching strategy rather than the content.

Happy to dig into the size one with you if you want a hand.

Comment on lines +1199 to +1203
return PNGPreview(
data=base64.b64encode(image).decode("ascii"),
width=image_width,
height=image_height,
)

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 block worries me a bit. With the shipped defaults I do not think a real chart can come back through this path at all.

ResponseSizeGuardMiddleware runs on every tool call, and get_chart_preview is in none of INFO_TOOLS, DATA_QUERY_TOOLS, COMMITTED_WRITE_TOOLS or STRING_FIELD_TRUNCATION_TOOLS (superset/mcp_service/utils/response_size_utils.py), and it is not in excluded_tools in MCP_RESPONSE_SIZE_CONFIG (superset/mcp_service/mcp_config.py). So _handle_oversized_response falls all the way through to the hard ToolError. The default budget is DEFAULT_MAX_RESPONSE_BYTES = 50_000.

I verified it on this branch. I rendered an ECharts line chart in headless Chromium at the default 800x600 with device_scale_factor=1, put those exact bytes into a PNGPreview inside a real ChartPreview, and called a tool named get_chart_preview through a real FastMCP client with the real middleware attached:

raw PNG bytes           :  96272
base64 chars            : 128364
measured response bytes : 128944
guard max_bytes         :  50000

is_error: True
"Response too large: 128,944 bytes (limit: 50,000) ... Reduction needed: ~61%"

The caller pays for the full Chromium launch and capture and then gets an error instead of the image, and that error suggests narrowing a query that has no narrowing knob. Sweeping the viewport on the same chart, 320x240 fit under the budget and 400x300 was already over, so there is no width/height inside your 64..4096 range that yields a useful preview.

Options, in the order I would pick them:

  • add get_chart_preview to excluded_tools in MCP_RESPONSE_SIZE_CONFIG, and say in the README that a PNG response is not bounded by max_bytes
  • or return the image as an MCP image content block instead of base64 inside the JSON payload, so it never counts against the text budget
  • or keep the guard and bound the output (downscale, or JPEG/WebP) with a documented ceiling

Whichever you pick, could we get a test in test_get_chart_preview.py that pushes a realistically sized PNG through ResponseSizeGuardMiddleware.on_call_tool and asserts a PNGPreview comes back rather than a ToolError? Every current PNG test stops at _generate_png_preview, which is exactly why this is invisible today. EMBEDDED_DIMENSIONS_MAX_PAGE_SIZE in superset/mcp_service/semantic_layer/schemas.py is a nice in repo example of budgeting a response against this same limit.

Or am I missing a place where the guard is relaxed for this tool?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3dbb1e1. The default response-size configuration now excludes get_chart_preview, and the README explicitly states that all preview formats are outside max_bytes unless the operator removes that exclusion. A real 800x600 PNG exceeding the default budget now traverses ResponseSizeGuardMiddleware.on_call_tool intact; the new test fails with ToolError on the previous code. The existing default-config assertion was updated to the new contract. All 308 preview, middleware and WebDriver tests and staged hooks pass.

so those stay server-side.
"""
try:
return await asyncio.to_thread(render)

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.

Not a blocker, and I am not reopening the cancellation point you already answered. This is a different property of the same call.

asyncio.to_thread submits to the loop's default executor (it is literally loop.run_in_executor(None, ...)), which is the same pool that superset/mcp_service/composite_token_verifier.py:184 and superset/mcp_service/guest_token_verifier.py:153 use for API key and guest token verification. That pool is min(32, cpu_count + 4) threads, and a render holds its thread for the whole capture, which by your own note is not released early when the request is cancelled. So a handful of concurrent format="png" calls can sit on the same threads every other request needs for transport auth.

Would a dedicated bounded executor, or an asyncio.Semaphore around the render, be worth it here? It also gives you a natural place to cap how many Chromium instances one process will launch at once, since _PlaywrightBrowserManager() is created per call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 3dbb1e1 with a dedicated two-worker PNG executor. It preserves caller ContextVars and leaves the default authentication pool available. The regression holds two render workers, cancels one awaiting request, verifies the third capture cannot start early, and confirms default-pool work still completes. That test fails on the old shared executor. Running captures still finish under the existing screenshot timeouts; the README documents this limit.

@bito-code-review

bito-code-review Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #e977bf

Actionable Suggestions - 0
Review Details
  • Files reviewed - 1 · Commit Range: 2dde258..100ee4c
    • superset/mcp_service/chart/schemas.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

@bito-code-review

bito-code-review Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #9d3525

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset/mcp_service/chart/tool/get_chart_preview.py - 2
    • Executor never shut down · Line 90-90
      `_PNG_EXECUTOR` is created at module scope and never shut down. Its non-daemon threads are joined by `concurrent.futures`' atexit hook, so an in-flight render blocks interpreter exit; a forking deployment (gunicorn `--preload`) inherits executor state without its threads. Consider an explicit shutdown hook or documenting the process-lifetime assumption.
    • Cancelled render holds pool slot · Line 1118-1121
      When an awaiting task is cancelled, `run_in_executor`'s future is cancelled but the worker thread keeps running the browser capture to completion, holding one of only 2 `_PNG_EXECUTOR` slots. A burst of cancelled requests (or a hung browser) can saturate the pool and block new renders. Consider a per-render timeout or documenting this tradeoff.
Review Details
  • Files reviewed - 4 · Commit Range: 100ee4c..3dbb1e1
    • superset/mcp_service/chart/tool/get_chart_preview.py
    • superset/mcp_service/mcp_config.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py
    • tests/unit_tests/mcp_service/test_middleware.py
  • Files skipped - 1
    • superset/mcp_service/README.md - Reason: Filter setting
  • 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

@deepujain

deepujain commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed the process-lifetime suggestion from Bito review #9d3525 in 136e405. The README now states that each MCP worker starts after any process fork and that interpreter shutdown waits for active captures. It also makes explicit that request cancellation does not stop the browser operation or release its worker slot early; captures retain the configured screenshot timeouts. This documents the existing tradeoff without changing execution behavior. Staged pre-commit hooks and the Markdown render pass.

The new CodeQL run failed before analysis in both languages because its init and analysis actions loaded different versions (4.38.0 and 4.38.1). This is an action-version mismatch, not a reported code finding; the workflow needs a rerun with consistent action versions.

@deepujain

Copy link
Copy Markdown
Contributor Author

Fixed the current inventory-budget failure in f62453d. The PNG description keeps its saved-chart, non-guest caller and unsupported-filter constraints with less repeated wording. The existing budget and serializer are unchanged.

The current-master integration reproduced the 3403 > 3400 failure before this patch; all 416 inventory, preview, schema and webdriver tests pass afterward. The original PR branch also passes 270 focused tests and staged pre-commit hooks. Only the two-line description change was pushed; the temporary upstream integration was used for validation. Hosted checks on the new head are pending.

@bito-code-review

bito-code-review Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #4c2c27

Actionable Suggestions - 0
Review Details
  • Files reviewed - 1 · Commit Range: 3dbb1e1..f62453d
    • superset/mcp_service/chart/tool/get_chart_preview.py
  • Files skipped - 1
    • superset/mcp_service/README.md - Reason: Filter setting
  • 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

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

Hi @deepujain, thanks for turning this around so quickly, and for doing both pieces properly rather than only the one I blocked on.

Clearing my original block: the PNG really does reach the caller now. I re-ran the exact round trip that failed last time and the caller gets a PNGPreview with byte identical data instead of a ToolError. The executor change is right too. I am leaving two comments, one of which I would still hold for: with the tool out of the size guard, nothing bounds the response in bytes any more, and the 4096 ceiling this PR allows reaches about two megabytes.

What I ran, on f62453d, in a clean uv venv:

  • pytest tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py tests/unit_tests/utils/webdriver_test.py tests/unit_tests/mcp_service/test_middleware.py -q, 308 passed, which matches your number exactly. Full tests/unit_tests/mcp_service: 4913 passed, 2 skipped.
  • Both new regression tests are genuinely load bearing, which I like. Reverting _run_png_render to asyncio.to_thread fails test_png_workers_do_not_starve_auth_or_release_on_cancellation, and dropping get_chart_preview back out of excluded_tools fails test_png_preview_passes_default_response_size_guard with Response blocked for get_chart_preview: 1921611 bytes exceeds limit of 50000.
  • Re-ran the authorization differential, since two new code paths on a caller scoped tool is where scoping tends to drift. It did not drift. A GuestUser is still refused both ways, UnsupportedFormat when guest_scope.is_guest_read() fires and Forbidden when EMBEDDED_SUPERSET is off so it does not fire, with no browser launched in either case. Two different callers still get distinct signed session cookies decoding to _user_id='11' and _user_id='29'. On the new executor the render still runs off the caller thread (mcp-png_0), sees g.user.id as the caller, does not inherit the request's g, calls raise_for_access exactly once, and leaves the caller's g intact afterwards.
  • Size behaviour: rendered real charts in headless Chromium and pushed each response through a real fastmcp.Client with the real ResponseSizeGuardMiddleware and your new default config. Numbers are in the inline comments.

CI is green on this head, 47 passing and nothing failing, including unit-tests (current), test-postgres (current), test-mysql, test-sqlite and pre-commit (current).

Comment on lines +1149 to +1152
if not (64 <= width <= 4096 and 64 <= height <= 4096):
return ChartError(
error="PNG dimensions must be between 64 and 4096 pixels.",
error_type="ValidationError",

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.

The PNG genuinely reaches the caller now, I confirmed that end to end, thank you. But with get_chart_preview out of the size guard, this clamp is the only thing left bounding the response, and it bounds pixels rather than bytes.

I rendered the same ECharts line chart at both corners of the range you allow here and pushed each result through a real fastmcp.Client with the real ResponseSizeGuardMiddleware and the new default excluded_tools:

viewport      pixel_density   response bytes   delivered
800x600             1                128,944   yes
4096x4096           1                859,666   yes
4096x4096           2              2,014,190   yes

pixel_density is not caller controlled, it comes from WEBDRIVER_WINDOW in superset/config.py, but a deployment that sets it to 2 doubles each rendered dimension, so that last row is reachable. All three came back with is_error=False and a base64 payload byte identical to what went in. A single tool result of roughly two megabytes of base64 text is the exact shape ResponseSizeGuardMiddleware was written to stop (its class docstring in superset/mcp_service/middleware.py says so), and an agent asked for a large preview will pass 4096 because the README advertises it.

Could we clamp the encoded output rather than only the viewport? A configurable byte or megapixel ceiling that either downscales before encoding or returns a ValidationError naming the largest size that fits would do it, with the ceiling written into the README next to the 64..4096 line. A test next to test_png_preview_passes_default_response_size_guard that renders at 4096x4096 and asserts the response stays under that ceiling would lock it in.

This is the only thing I would still hold for, and I am happy to help if the clamp turns out to be awkward to place.

Comment thread superset/mcp_service/mcp_config.py Outdated
"generate_explore_link", # Returns URLs
"open_sql_lab_with_context", # Returns URLs
"search_tools", # Returns tool schemas for discovery (intentionally large)
"get_chart_preview", # Rendered PNG previews exceed the text budget

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.

Not a blocker, but worth on the record because it reaches past PNG.

The guard is keyed on the tool name, so this entry also turns it off for ascii, table and vega_lite. ascii and table are row capped at 50 and 20 so they are fine, but vega_lite runs at _preview_row_limit(form_data, 1000) and then embeds the whole result set inline as "data": {"values": data}, so it can get large. I built a 1000 row spec of plausible chart columns and ran it through the same real client twice:

1000 row vega_lite preview: 142,677 bytes

  with master's excluded_tools -> is_error=True   (blocked at 50,000)
  with this branch's list      -> is_error=False  (130,618 bytes delivered)

Also worth knowing: #39719 removed get_chart_preview from this list on purpose and added test_default_config_checks_chart_preview to pin it, which is the assertion this PR inverts. That decision was about the non PNG formats, so it may be worth looping in whoever owns it before flipping it.

If you would rather keep that invariant, skipping the guard on the payload's content.type == "png" instead of on the tool name gives PNG its escape hatch and leaves the other three formats checked.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 30, 2026
@deepujain
deepujain force-pushed the feat/mcp-png-chart-preview-41114 branch from f62453d to 5d6c555 Compare October 1, 2026 05:06
deepujain added a commit to deepujain/superset that referenced this pull request Oct 1, 2026
raise_for_access raises SupersetSecurityException inside the render
worker, where the broad except Exception handler converted it to
RenderError. Catch it explicitly so denied charts return Forbidden.

Tighten test_png_preview_authorizes_before_browser: the denied case
now raises the real SupersetSecurityException and asserts the
Forbidden contract instead of accepting any ChartError.

Addresses the codeant-ai findings on apache#44558.
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 1, 2026
@bito-code-review

bito-code-review Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #7621c3

Actionable Suggestions - 0
Additional Suggestions - 6
  • superset/mcp_service/middleware.py - 1
    • PNG budget scope gap · Line 2089-2100
      The new PNG budget only triggers when `tool_name == "get_chart_preview"`. `generate_chart` and `update_chart` also produce PNG previews via `_get_chart_preview_internal` (generate_chart.py:728, update_chart.py:1072) and embed `preview_result.content` in their response, so a legitimate PNG from those tools still trips the 50KB `max_bytes` guard and is rejected. Consider keying the budget on the returned payload's `content.type == "png"` rather than the tool name.
  • tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py - 5
    • Duplicated test setup boilerplate · Line 2013-2032
      These new tests copy the ~20-line stub setup (`security_manager`, `find_chart_by_identifier`, `guest_scope.is_guest_read`, `is_feature_enabled`, `get_url_path`, `_PlaywrightBrowserManager`, `WebDriverPlaywright`) already repeated in `test_png_preview_authorizes_before_browser` and siblings — grep counts 6 identical `g.user = SimpleNamespace` / webdriver-patch blocks in this file. A shared fixture yielding the mocks would make future auth-stub changes a one-edit fix. Twin block at lines 2050-2063.
    • Missing docstring and return type · Line 2217-2217
      BITO.md rule 12148 requires every newly added test function to include a docstring, and rule 7819 requires an explicit `-> None` return annotation. This test documents the end-to-end PNG budget over a real `FastMCP` transport; capture that intent in a docstring and add the return annotation to match the org standard for new test code.
    • Hardcoded limit duplicates constant · Line 2242-2242
      The regex pins `1000000` as a bare literal, duplicating `DEFAULT_MAX_PNG_RESPONSE_BYTES` (superset/mcp_service/constants.py:32, verified against the message built in `ResponseSizeGuardMiddleware.on_call_tool`). If the default changes, this test fails with an opaque regex mismatch instead of pointing at the named constant. Build the pattern from the constant so the pin stays tied to its source.
    • Missing docstring and return type · Line 2251-2251
      Same BITO.md rules 12148 and 7819 apply: this new test needs a docstring and an explicit `-> None` return annotation. The inline comment at line 2264 already states the scenario (the returned format, not caller-supplied arguments, chooses the limit) — promote it into the docstring where rule 12148 says test intent belongs.
    • Missing docstring and return type · Line 2273-2273
      BITO.md rules 12148 and 7819 apply here too: add a docstring and an explicit `-> None` return annotation. The boundary semantics this test pins (strict `>` comparison in `ResponseSizeGuardMiddleware.on_call_tool`, so an exactly-at-budget PNG passes) are worth one line of documentation.
Review Details
  • Files reviewed - 8 · Commit Range: 8f9d2cd..5d6c555
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_preview.py
    • superset/mcp_service/constants.py
    • superset/mcp_service/mcp_config.py
    • superset/mcp_service/middleware.py
    • superset/utils/webdriver.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_preview.py
    • tests/unit_tests/mcp_service/test_middleware.py
  • Files skipped - 1
    • superset/mcp_service/README.md - Reason: Filter setting
  • 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

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

Hi @deepujain, this is the right fix, and it is a better one than I asked for. Taking the tool back out of excluded_tools and giving PNG its own png_max_bytes budget keyed on the returned content.type is exactly the narrow shape, and it restores the invariant the April change had pinned instead of leaving it inverted.

Approving. The response is bounded in bytes now, the whole documented 64..4096 range fits at the default pixel_density, and over the ceiling the caller gets an actionable error naming the actual size and two ways out. One NIT inline, which should not hold the PR.

What I ran, on 5d6c555, in a fresh uv venv I checked was genuinely isolated (include-system-site-packages = false, every package resolving from the venv, verified from outside the repo after being bitten by a fake one last round):

  • The three affected suites, 319 passed. Full tests/unit_tests/mcp_service, 6309 passed and 4 skipped.
  • Pushed real headless-Chromium renders of the same chart through a real fastmcp.Client with the real middleware built from the real default config, sweeping to the boundary rather than trusting the documented range:
  800x600   d1    128,944 bytes   delivered, data intact
 2048x2048  d1    416,158         delivered
 3000x3000  d1    613,754         delivered
 3500x3500  d1    729,678         delivered
 4096x4096  d1    859,666         delivered, data intact
 2048x2048  d2    962,026         delivered
 2300x2300  d2  1,076,698         rejected
 4096x4096  d2  2,014,190         rejected

The real ceiling at pixel_density: 2 for this chart sits between 2048 and 2300 square, inside the documented range, which is the case your new README paragraph already calls out. Every delivered payload decoded byte identical to what went in, so nothing is being silently rescaled.

  • vega_lite got its protection back. The same 1000 row spec, 142,677 bytes, is blocked again at max_bytes, and test_default_config_checks_chart_preview is restored to its original name, docstring and assertion.
  • Both halves are pinned. Deleting the PNG branch from the guard fails 4 tests; putting get_chart_preview back into excluded_tools fails 4 more, including the April one.
  • The new branch reads content off the extracted payload, so I checked it both ways: structured_output_enabled True and False both deliver, 128,904 bytes each.
  • Re-ran the authorization differential, fourth round, still no drift. Guest refused both ways with no browser launched, two callers still getting distinct signed sessions (_user_id='11' and _user_id='29'), and the render still on mcp-png_0 seeing g.user.id as the caller, not inheriting the request's g, and calling raise_for_access once. Encoding is still inside the worker with the clamp downstream of it.

On CI: 44 passing, and the single red check is playwright-tests (chromium, /app/prefix), which itself reports 1 flaky, 89 passed and zero failures. The flake is a 422 on the fixture creating apiRequestContext.post in chart-list.spec.ts:49, it passed on retry inside that same job, and playwright-tests (chromium) passed on this commit. I am confident it has nothing to do with this change, but playwright-tests-required will need a re-run to go green before merge.

Comment on lines +2094 to +2100
if actual_bytes > self.png_max_bytes:
raise ToolError(
f"PNG preview is {actual_bytes} bytes, exceeding the "
f"{self.png_max_bytes}-byte limit. Request smaller width and "
"height, or use the URL preview format."
)
return response

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.

Just a small NIT, not worth another round on its own.

This branch returns before the warn log and raises without the event_logger.log(action="mcp_response_size_exceeded", ...) that the text path uses at line 2031, so a rejected PNG is completely silent. Same harness, both paths:

PNG over png_max_bytes -> is_error=True, middleware size/block log lines: NONE
ascii over max_bytes   -> "Response size warning for get_chart_preview: 200588 bytes (401% of 50000 limit)"
                          "Response blocked for get_chart_preview: 200588 bytes exceeds limit of 50000"

Every rejection costs a full Chromium capture, so this is the one case most worth a counter, and there is no 80 percent warning against png_max_bytes the way warn_threshold gives one against max_bytes. A logger.warning plus the same event_logger.log call would cover both.

Tiny second thing while you are in here: "get_chart_preview" is the only tool name hardcoded in this middleware, while every other grouping lives as a named frozenset in response_size_utils.py. Nothing is wrong today, PNGPreview is only reachable from this tool, but a constant there would match the house style.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 9, 2026
Signed-off-by: Deepak Jain <deepujain@gmail.com>
raise_for_access raises SupersetSecurityException inside the render
worker, where the broad except Exception handler converted it to
RenderError. Catch it explicitly so denied charts return Forbidden.

Tighten test_png_preview_authorizes_before_browser: the denied case
now raises the real SupersetSecurityException and asserts the
Forbidden contract instead of accepting any ChartError.

Addresses the codeant-ai findings on apache#44558.

Signed-off-by: Deepak Jain <deepujain@gmail.com>
The Forbidden-mapping except branch pushed _generate_png_preview to
complexity 11. Moving the asyncio.to_thread call plus its two except
branches into _run_png_render keeps the error contract identical and
restores the function under the limit.

Signed-off-by: Deepak Jain <deepujain@gmail.com>
Upstream apache#44574 landed the messages.pot regeneration on master; the
babel-extract and unit-tests (check_pot_drift_test.py) failures on this PR
were base drift, not contributor code. Fresh CI will run against the new
merge commit with the fixed base pot.

Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Signed-off-by: Deepak Jain <deepujain@gmail.com>
@deepujain
deepujain force-pushed the feat/mcp-png-chart-preview-41114 branch from 5d6c555 to 1478574 Compare October 9, 2026 22:55
@codeant-ai-for-open-source

Copy link
Copy Markdown
Contributor

CodeAnt PR Risk: Low Risk

  • The PR appears safe to merge; PNG previews check caller access and use an isolated browser with a separate response-size limit.
  • Tests cover authorization, cleanup, cancellation behavior, and response-size limits; the documented cancellation delay is bounded by screenshot timeouts.

Assessed commit: 1478574095b5

@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 10, 2026
from superset import is_feature_enabled
from superset.mcp_service.auth import _mcp_tool_call_context
from superset.utils.core import override_user
from superset.utils.screenshots import validate_screenshot_image

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

superset/utils/screenshots.py already defines PNG_MAGIC_BYTES, used by validate_screenshot_image right below this. Importing it here too avoids the two checks drifting if that byte sequence ever changes.

Suggested change
from superset.utils.screenshots import validate_screenshot_image
from superset.utils.screenshots import PNG_MAGIC_BYTES, validate_screenshot_image

image = driver.get_screenshot(url, "chart-container", user=user)
if (
image is None
or not image.startswith(b"\x89PNG\r\n\x1a\n")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
or not image.startswith(b"\x89PNG\r\n\x1a\n")
or not image.startswith(PNG_MAGIC_BYTES)

@@ -1447,7 +1575,11 @@ def __init__(self, form_data: Dict[str, Any]):
action="mcp.get_chart_preview.preview_generation"
):
preview_generator = PreviewFormatGenerator(chart, request)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

NIT: preview_generator gets built and then thrown away on the png branch. Not a big deal, but may as well only construct it where it's used.

Suggested change
preview_generator = PreviewFormatGenerator(chart, request)
content = (
await _generate_png_preview(chart.id, request)
if request.format == "png"
else PreviewFormatGenerator(chart, request).generate()

@rusackas

Copy link
Copy Markdown
Member

Nothing to add on top of @gabotorresruiz's review, honestly, that was about as thorough a pass as this gets. Re-ran the suite myself and traced the executor, the Forbidden mapping, and the new byte-bounded guard, and it all holds up.

A couple of small things that shouldn't hold this up, left as suggestions. One more that doesn't land in this diff so I can't suggest it inline: DEFAULT_INSTRUCTIONS in app.py still only mentions ASCII/URL/table/Vega-Lite, not the new png format, which the MCP README's own checklist calls for. Worth a quick follow-up whenever you're back in here.

Approving.

@rusackas
rusackas merged commit 1e18d01 into apache:master Oct 20, 2026
73 checks passed
rebenitez1802 added a commit that referenced this pull request Oct 20, 2026
Master's PNG chart previews (#44558) grew the chart tool definitions
without refreshing the native tool inventory, which fails the unit suite
once merged here. Apply the generate_chart, get_chart_preview and
update_chart entries from #45511 unchanged and recompute the catalog
totals to include this branch's get_dashboard_info growth.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants