feat(mcp): chart type plugin registry for extensible generate_chart#39922
Conversation
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The inline context is insufficient to determine what 'this' refers to in the question. No diff hunk or suggestion snippet is provided for the thread in superset/mcp_service/chart/chart_utils.py. |
There was a problem hiding this comment.
Pull request overview
This PR introduces a chart-type plugin architecture for the MCP chart pipeline, centralizing per-chart-type behavior (pre-validation, column extraction/normalization, form_data mapping, runtime warnings, naming, viz_type resolution) into one plugin per supported chart_type. This reduces the previous need to update multiple dispatch sites when adding or modifying chart types.
Changes:
- Added a
ChartTypePluginprotocol +BaseChartPlugin, a global plugin registry, and 7 built-in chart plugins (xy,table,pie,pivot_table,mixed_timeseries,handlebars,big_number). - Refactored schema/dataset/runtime validation and form_data mapping to dispatch via the registry instead of
isinstance/if/elifchains. - Enhanced tool/docs output by expanding
generate_chartdocstring chart types and addingchart_type_display_nametoChartInfo.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| superset/mcp_service/chart/validation/schema_validator.py | Switch pre-validation dispatch to registry/plugins; refactor Pydantic error enhancement. |
| superset/mcp_service/chart/validation/runtime/init.py | Replace XY-only runtime checks with per-plugin runtime warning dispatch. |
| superset/mcp_service/chart/validation/dataset_validator.py | Delegate column extraction/normalization to plugins; apply aggregation validation for all chart types. |
| superset/mcp_service/chart/tool/generate_chart.py | Update tool docstring to list all supported chart_type values. |
| superset/mcp_service/chart/schemas.py | Add chart_type_display_name to ChartInfo; resolve display name via registry mapping. |
| superset/mcp_service/chart/registry.py | Introduce global registry for chart plugins + viz_type display-name lookup. |
| superset/mcp_service/chart/plugin.py | Add plugin protocol and base class defining the plugin responsibilities. |
| superset/mcp_service/chart/plugins/init.py | Register the built-in plugins on import. |
| superset/mcp_service/chart/plugins/xy.py | Implement XY plugin (pre-validate, refs, mapping, naming, viz_type, runtime warnings). |
| superset/mcp_service/chart/plugins/table.py | Implement Table plugin. |
| superset/mcp_service/chart/plugins/pie.py | Implement Pie plugin. |
| superset/mcp_service/chart/plugins/pivot_table.py | Implement Pivot Table plugin. |
| superset/mcp_service/chart/plugins/mixed_timeseries.py | Implement Mixed Timeseries plugin. |
| superset/mcp_service/chart/plugins/handlebars.py | Implement Handlebars plugin. |
| superset/mcp_service/chart/plugins/big_number.py | Implement Big Number plugin including post-map trendline temporal validation. |
| superset/mcp_service/chart/chart_utils.py | Replace mapping/name/viz_type dispatch with registry/plugin calls. |
| superset/mcp_service/app.py | Import plugins at MCP app startup; update default instructions about chart type naming. |
891676f to
1e2b541
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #39922 +/- ##
==========================================
- Coverage 64.54% 59.16% -5.39%
==========================================
Files 2665 820 -1845
Lines 146641 70060 -76581
Branches 33880 8759 -25121
==========================================
- Hits 94652 41450 -53202
+ Misses 50271 26874 -23397
- Partials 1718 1736 +18
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Internal architecture review — findings & resolutions Ran an independent codex review of the plugin registry design. Summary of findings and their status:
The thread-safety fix ( |
f5ba09f to
7dc3563
Compare
7dc3563 to
2e7a86f
Compare
There was a problem hiding this comment.
Code Review Agent Run #8680f9
Actionable Suggestions - 7
-
superset/mcp_service/chart/schemas.py - 1
- CWE-390: Bare Exception Handler · Line 491-496
-
superset/mcp_service/chart/plugin.py - 1
- Invalid type annotation string · Line 175-175
-
superset/mcp_service/chart/tool/update_chart.py - 2
- Redundant DB query in validation · Line 203-204
- Missing test coverage for normalization · Line 416-432
-
superset/mcp_service/chart/plugins/handlebars.py - 1
- Duplicate validation with schema · Line 44-124
-
tests/unit_tests/mcp_service/chart/validation/test_runtime_validator.py - 1
- Test coverage regression · Line 213-234
-
superset/mcp_service/chart/validation/runtime/__init__.py - 1
- Avoid catching blind Exception · Line 107-107
Additional Suggestions - 6
-
superset/initialization/__init__.py - 1
-
Missing error handling for MCP init · Line 750-750Wrap the call to `configure_mcp_chart_registry()` in a try-except block to catch any exceptions from `registry.configure()` and log them so that `init_app()` can continue even if MCP chart registry configuration fails.
-
-
superset/mcp_service/chart/validation/schema_validator.py - 2
-
Import inside function body · Line 151-152The `from superset.mcp_service.chart.registry import get_registry` import is placed inside the function body at lines 151-152. This causes the import to execute on every call to `_pre_validate_chart_type`. Move it to the module-level imports for better performance.
Code suggestion
--- superset/mcp_service/chart/validation/schema_validator.py +++ superset/mcp_service/chart/validation/schema_validator.py @@ -29,6 +29,7 @@ from superset.mcp_service.chart.schemas import ( ) from superset.mcp_service.common.error_schemas import ChartGenerationError +from superset.mcp_service.chart.registry import get_registry logger = logging.getLogger(__name__) @@ -148,9 +149,6 @@ class SchemaValidator: config: Dict[str, Any], ) -> Tuple[bool, ChartGenerationError | None]: """Validate chart type and dispatch to plugin pre-validation.""" - from superset.mcp_service.chart.registry import get_registry - registry = get_registry()
-
Misleading variable name · Line 176-176Variable `valid_types` on line 176 is misleading — `registry.all_types()` returns only enabled chart types (per registry.py:168), so the name suggests all registered types when it actually contains only enabled ones. Rename to `enabled_types` for clarity.
Code suggestion
--- superset/mcp_service/chart/validation/schema_validator.py (lines 175-182) +++ superset/mcp_service/chart/validation/schema_validator.py @@ -173,12 +173,12 @@ class SchemaValidator: ) if not registry.is_enabled(chart_type): - valid_types = ", ".join(registry.all_types()) + enabled_types = ", ".join(registry.all_types()) return False, ChartGenerationError( error_type="disabled_chart_type", message=f"Chart type '{chart_type}' is not enabled on this instance", details=f"Chart type '{chart_type}' is registered but has been " f"disabled by the operator. " - f"Enabled chart types: {valid_types}", + f"Enabled chart types: {enabled_types}", suggestions=[ - f"Use one of the enabled chart types: {valid_types}", + f"Use one of the enabled chart types: {enabled_types}", "Contact your administrator if you believe this is an error", ], error_code="DISABLED_CHART_TYPE", )
-
-
superset/mcp_service/chart/plugins/xy.py - 3
-
Private member access in DatasetValidator · Line 114-114Accessing private method `_get_canonical_column_name`. Consider using a public API or adding a public wrapper method.
-
Private member access in DatasetValidator · Line 115-115Accessing private method `_get_canonical_metric_name`. Consider using a public API or adding a public wrapper method.
-
Private member access in DatasetValidator · Line 129-129Accessing private method `_normalize_filters`. Consider using a public API or adding a public wrapper method.
-
Filtered by Review Rules
Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.
-
superset/mcp_service/chart/registry.py - 1
- Broad exception catch · Line 87-87
Review Details
-
Files reviewed - 27 · Commit Range:
d3af275..f90f1cd- superset/initialization/__init__.py
- superset/mcp_service/app.py
- superset/mcp_service/chart/chart_utils.py
- superset/mcp_service/chart/plugin.py
- superset/mcp_service/chart/plugins/__init__.py
- superset/mcp_service/chart/plugins/big_number.py
- superset/mcp_service/chart/plugins/handlebars.py
- superset/mcp_service/chart/plugins/mixed_timeseries.py
- superset/mcp_service/chart/plugins/pie.py
- superset/mcp_service/chart/plugins/pivot_table.py
- superset/mcp_service/chart/plugins/table.py
- superset/mcp_service/chart/plugins/xy.py
- superset/mcp_service/chart/registry.py
- superset/mcp_service/chart/schemas.py
- superset/mcp_service/chart/tool/generate_chart.py
- superset/mcp_service/chart/tool/update_chart.py
- superset/mcp_service/chart/validation/dataset_validator.py
- superset/mcp_service/chart/validation/runtime/__init__.py
- superset/mcp_service/chart/validation/schema_validator.py
- superset/mcp_service/flask_singleton.py
- superset/mcp_service/mcp_config.py
- tests/unit_tests/mcp_service/chart/test_big_number_chart.py
- tests/unit_tests/mcp_service/chart/test_registry.py
- tests/unit_tests/mcp_service/chart/test_registry_filters.py
- tests/unit_tests/mcp_service/chart/tool/test_update_chart.py
- tests/unit_tests/mcp_service/chart/validation/test_column_name_normalization.py
- tests/unit_tests/mcp_service/chart/validation/test_runtime_validator.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
The rebase of the self-review commit left conflict markers in mcp_config.py; resolve in favor of master's get_mcp_api_key_enabled based factory.
If two plugins claim the same viz_type, display_name_for_viz_type() silently resolves to the iteration-order winner. Surface a warning at register() time so plugin authors catch the shadowing immediately.
Earlier style commits reformatted 24 unrelated files with a newer local ruff than the pinned 0.9.7, removed the PT004 ignore from pyproject.toml, and the initial branch commit accidentally deleted tests/unit_tests/utils/test_split.py. None of these belong to the chart plugin registry change; restore them to master to keep the PR scoped.
…p_service create_app() called configure_mcp_chart_registry() unconditionally, which imported mcp_config and its top-level fastmcp dependency, breaking plain Superset installs without the [fastmcp] extra. Move the registry configure call entirely into flask_singleton.py (both the standalone and reused-app paths), making it the single configure site and resolving the dual-site review finding.
Three pre_validate methods were checking only canonical field names, but
the Pydantic schemas accept validation aliases. For example
PieChartConfig.dimension accepts "groupby" as an alias, so sending
{"groupby": ...} would pass the schema but be incorrectly rejected by
pre_validate.
Four normalize_column_refs implementations did not guard against
sql_expression metrics (name=None), causing AttributeError when
_get_canonical_column_name(None, ...) was called. XYChartPlugin already
handled this correctly; the fix brings the other plugins in line.
Pre-validate alias fixes:
- TableChartPlugin: accept columns/all_columns/groupby (AliasChoices)
- PieChartPlugin: accept dimension/groupby (AliasChoices)
- PivotTableChartPlugin: accept rows/groupby/dimension (AliasChoices)
sql_expression normalization guards:
- BigNumberChartPlugin: skip metric normalization when sql_expression set
- PieChartPlugin: same for metric field
- MixedTimeseriesChartPlugin: skip in _norm_list helper
- PivotTableChartPlugin: skip in _norm_col_list helper
… effective dataset on rebind Three bugs identified by codeant-ai review (June 26): 1. update_chart.py: column normalization used chart.datasource_id even when request.dataset_id was provided for a rebind, resolving names against the wrong schema. Now uses effective_norm_dataset_id (request.dataset_id when set, else chart.datasource_id). 2. table.py normalize_column_refs: lacked the sql_expression guard present in xy.py and mixed_timeseries.py. A sql_expression ColumnRef has name=None, causing _get_canonical_column_name(None, ...) to crash. Added elif guard. 3. handlebars.py _norm_list: same issue — sql_expression metrics in aggregate mode have name=None. Added elif guard matching the other plugins. All three bugs have accompanying regression tests.
… viz_type override - PieChartConfig: reject saved_metric=True on dimension (schema validator + defensive guard in normalize_column_refs) — mirrors existing sql_expression rejection - get_chart_info: recompute chart_type_display_name when viz_type is overridden from cached unsaved-state form_data so the two fields stay in sync - Add regression tests for both fixes
…thods as public API - Move build_dataset_context_from_orm from compile.py into dataset_validator.py as the single authoritative ORM→DatasetContext converter; fixes a latent bug where the compile.py copy used `or ""` for database_name but _get_dataset_context could pass None to the required str field - Simplify DatasetValidator._get_dataset_context to delegate to the shared helper - Rename DatasetValidator._get_canonical_column_name / _get_canonical_metric_name / _normalize_filters to drop the underscore prefix — these are the public API that all seven plugins call, and the _ convention implied (incorrectly) that they were private implementation details - Annotate native_viz_types as ClassVar[Mapping[str, str]] in the Protocol, BaseChartPlugin, and all 7 concrete plugins — prevents accidental mutation of the shared default dict and matches the read-only contract (Mapping, not dict) - Update all call sites (7 plugins + 3 test files) to use the new names/import path Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…py function export `import superset.mcp_service.chart.tool.get_chart_info as module` resolves to the get_chart_info *function* rather than the module because chart/tool/__init__.py imports the function under the same name, overwriting the submodule attribute on the parent package. Replace with get_chart_info_module (already imported via importlib at module level), which correctly resolves to the module object.
4f64995 to
b4e1f5d
Compare
| return display_name_for_viz_type(viz_type) | ||
|
|
||
|
|
||
| _PROXY = _RegistryProxy() |
There was a problem hiding this comment.
Suggestion: Add an explicit type annotation to this module-level proxy instance to comply with the variable type-hint rule. [custom_rule]
Severity Level: Minor
Why it matters? 🤔
This is a module-level variable that can be annotated, and it is currently assigned without an explicit type hint. That is a valid type-hint rule violation.
(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/registry.py
**Line:** 274:274
**Comment:**
*Custom Rule: Add an explicit type annotation to this module-level proxy instance to comply with the variable type-hint rule.
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 fixThere was a problem hiding this comment.
Declining — Minor
…nine) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fitzee
left a comment
There was a problem hiding this comment.
Review (dual pass: Claude + Codex)
Re-reviewed from scratch given the PR has grown substantially since an earlier draft-stage pass (39 commits, now non-draft). All four earlier blocking concerns (registry write-lock, column-name sanitization vs #39915, the 5-type validation claim, and plugin-load retry storm) were re-verified against the current diff and live source rather than taken from the PR description at face value.
Verdict: Ship it with nits — two-way door (code-only, no schema/data migration), no CRITICAL/HIGH findings.
Resolved from the earlier pass
register()now writes under_plugins_lock(RLock), covering the collision-check loop too. Well tested.- The regex removal on
ColumnRef.name/FilterConfig.column/BigNumberChartConfig.temporal_columnis backed bysanitize_user_input(check_sql_keywords=True)on all three — confirmed by readingschemas.pydirectly. Security boundary intact. - Plugin-load circuit breaker (
_plugins_load_failed) added, plus rollback of partially-registered plugins on a failed import. Well tested.
One correction worth making to the PR description
The "5 of 7 chart types silently skipped column validation" framing doesn't match master: dataset_validator.py::_extract_column_references() and schema_validator.py's dispatch dict already had explicit branches for all 7 types before this PR — the pre-PR docstring even says so directly ("Covers every supported ChartConfig variant ... not just XY and table"). Column-existence validation for all 7 types was already live.
What's actually new (and a legitimate fix): normalize_column_names() previously only corrected column-name casing for 2 of 7 types (XY, Table); this PR extends that to all 7 via plugin.normalize_column_refs(). That's backward-compatible — normalization only fixes casing, it can't newly reject a valid column — so the risk here is much lower than "breaking behavioral change" implies. Worth updating the description so future readers aren't misled, but not a merge blocker.
Remaining nits
- MEDIUM —
pivot_table.py'snormalize_column_refs()(rows/columns/metrics) has no test coverage. It's the newly-normalized type with the most fields; worth a test before merge. - LOW —
XYChartPlugin.get_runtime_warnings()merges cardinalitysuggestionsinto thewarningslist instead of keeping them separate (plugins/xy.py). Onmaster,_validate_cardinality()returned warnings and suggestions as distinct lists that landed in separate response fields. The newChartTypePlugin.get_runtime_warnings() -> list[str]protocol signature has no channel for suggestions at all, so every plugin is structurally stuck conflating the two. Worth widening the protocol return type (e.g. a small dataclass or(warnings, suggestions)tuple) before other plugins grow suggestion logic. - LOW — the "Scope note" says ~25 unrelated formatting-churn files were restored to match master; 14 still remain in the diff (
advanced_data_type/plugins/*,datasets/api.py,datasource/api.py,utils/excel.py,utils/mock_data.py,views/log/api.py, and a handful of unrelated test files). Small (a few lines each) but worth dropping to keep the diff scoped to the stated intent. - LOW — the description says
registry.configure()is wired fromsuperset/initialization/__init__.pyandflask_singleton.py; both call sites are actually inflask_singleton.py(whose own comment says it's "the registry's only configure site"). The wiring itself is correct and complete — verified all three branches of the app-boot logic — just the location claim is off.
What's sound
The plugin consolidation is the right call: four previously out-of-sync dispatch tables collapse into one ChartTypePlugin Protocol + BaseChartPlugin, the registry self-bootstraps lazily with no circular-import issues, and the operator kill-switch (MCP_DISABLED_CHART_PLUGINS / MCP_CHART_PLUGIN_ENABLED_FUNC) swaps its filter config atomically. The locking, sanitization, and circuit-breaker fixes since the last pass show real engineering, not minimal patches.
None of the above blocks merge. Recommend closing the pivot_table test gap and the warnings/suggestions split before merge, and tightening the description in a follow-up.
- Widen get_runtime_warnings() Protocol return type to tuple[list[str], list[str]] so cardinality suggestions are routed separately from warnings (fitzee LOW) - Add PivotTableChartPlugin normalize_column_refs tests: rows, metrics, columns, filters, saved_metric, sql_expression skip, multiple-entry normalization (fitzee MEDIUM) - Restore 7 formatting-churn files (internet_address, internet_port, datasets/api, datasource/api, utils/excel, utils/mock_data, views/log/api) to master state (fitzee LOW) - Fix PR description: clarify normalization gap (not existence-check gap), correct registry.configure() wiring location to flask_singleton.py only (fitzee LOW) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Thanks @fitzee — all four items addressed in 9e55ba0: MEDIUM — LOW — LOW — 14 formatting-churn files: LOW — PR description corrections:
|
SUMMARY
Introduces a
ChartTypePluginprotocol and central registry that consolidates all chart-type-specific logic into a single plugin class per chart type. Previously, adding a new chart type required synchronised edits to four separate dispatch locations. With this change it requires one new file.The problem: 4 dispatch points that had to stay in sync
Before this PR,
generate_chartlogic was split across:schema_validator.py_pre_validate_*static methods, one per typedataset_validator.pyisinstancebranches in_extract_column_referencesandnormalize_column_nameschart_utils.pyif/elifchain inmap_config_to_form_data,generate_chart_name,_resolve_viz_typeruntime/__init__.pyisinstance(config, XYChartConfig)hardcoded branchResult: column name normalization (case-insensitive matching) was only wired for XY and Table (2 of 7). The 5 remaining types (
pie,pivot_table,mixed_timeseries,handlebars,big_number) skipped normalization — case mismatches surfaced only at query time. Column existence checks were already in place for all 7 types via the previous dispatch table.Architecture
New file layout
Request flow (before → after)
flowchart LR subgraph BEFORE["Before: 4 separate dispatch points"] direction TB A1[schema_validator.py\n7 pre_validate methods] A2[dataset_validator.py\nisinstance branches] A3[chart_utils.py\nif/elif chains] A4[runtime/__init__.py\nXY hardcoding] end subgraph AFTER["After: single registry lookup"] direction TB B1[registry.get chart_type] --> B2[plugin.pre_validate] B1 --> B3[plugin.extract_column_refs] B1 --> B4[plugin.to_form_data] B1 --> B5[plugin.post_map_validate] B1 --> B6[plugin.normalize_column_refs] B1 --> B7[plugin.get_runtime_warnings] B1 --> B8[plugin.generate_name] B1 --> B9[plugin.resolve_viz_type] end BEFORE -.->|refactored to| AFTERPlugin protocol
classDiagram class ChartTypePlugin { <<Protocol>> +str chart_type +str display_name +dict native_viz_types +pre_validate(config) ChartGenerationError|None +extract_column_refs(config) list[ColumnRef] +to_form_data(config, dataset_id) dict +post_map_validate(config, form_data, dataset_id) ChartGenerationError|None +normalize_column_refs(config, dataset_context) Any +get_runtime_warnings(config, dataset_id) list[str] +generate_name(config, dataset_name) str +resolve_viz_type(config) str +schema_error_hint() ChartGenerationError|None } class BaseChartPlugin { +pre_validate() None +extract_column_refs() [] +to_form_data() NotImplementedError +post_map_validate() None +normalize_column_refs() config unchanged +get_runtime_warnings() [] +generate_name() "Chart" +resolve_viz_type() "unknown" +schema_error_hint() None +_with_context(what, context)$ str } class XYChartPlugin class PieChartPlugin class TableChartPlugin class BigNumberChartPlugin class PivotTableChartPlugin class MixedTimeseriesChartPlugin class HandlebarsChartPlugin ChartTypePlugin <|.. BaseChartPlugin BaseChartPlugin <|-- XYChartPlugin BaseChartPlugin <|-- PieChartPlugin BaseChartPlugin <|-- TableChartPlugin BaseChartPlugin <|-- BigNumberChartPlugin BaseChartPlugin <|-- PivotTableChartPlugin BaseChartPlugin <|-- MixedTimeseriesChartPlugin BaseChartPlugin <|-- HandlebarsChartPluginRegistry bootstrap (lazy, no circular imports)
sequenceDiagram participant Caller as Caller (test / tool) participant Registry as registry.py participant Plugins as plugins/__init__.py Caller->>Registry: get("xy") Registry->>Registry: _ensure_plugins_loaded() alt first call Registry->>Plugins: import (triggers register() calls) Plugins-->>Registry: 7 plugins registered end Registry-->>Caller: XYChartPlugin instanceThe registry self-bootstraps on first lookup — no dependency on
app.pybeing imported first, so isolated unit tests work without the full Flask app.Operator controls (runtime kill switch)
Two config knobs (defaults in
mcp_config.py, overridable insuperset_config.py) let operators filter chart plugins without code changes:MCP_DISABLED_CHART_PLUGINS: frozenset[str]frozenset({"handlebars"})as an emergency kill switchMCP_CHART_PLUGIN_ENABLED_FUNC: Callable[[str], bool] | Noneregistry.configure()is invoked solely fromflask_singleton.pyat two points: once after the base app config is loaded, and once after the standalone MCP service layers its own config overlay viaget_mcp_config(). Both call sites are inflask_singleton.py— it is the registry's only configure site (noted in the file's inline comment). Filter state lives in an immutable dataclass swapped atomically, so concurrent readers never observe a torn(disabled, enabled_func)pair. A disabled chart type returns a structuredDISABLED_CHART_TYPEerror, distinct fromINVALID_CHART_TYPEfor unknown types.Registry hardening
register()runs under the registry lock (anRLock, so plugins registering during the lazy import don't deadlock) and warns when a plugin claimsnative_viz_typesalready owned by another plugin (display-name lookups resolve to the earlier registration)._plugins_load_failed) and rolls back partially-registered plugins — subsequent lookups returnNoneinstead of re-attempting the import on every call.How to add a new chart type
Everything goes in one new file. Here is a minimal working example for a hypothetical
funnelchart:Step 1 — Add the Pydantic schema (
chart/schemas.py):Step 2 — Create the plugin (
chart/plugins/funnel.py):Step 3 — Register it (
chart/plugins/__init__.py):Step 4 (optional) — Custom Pydantic error hint: override
schema_error_hint()in the same plugin file.SchemaValidatorresolves hints through the registry when Pydantic fails to parse the config union, so no validator edits are needed:That is the complete change set — no edits to
chart_utils.py,dataset_validator.py,schema_validator.py, orruntime/__init__.pydispatch logic.Bug fixes included
pie,pivot_table,mixed_timeseries,handlebars,big_number) now normalize column case too. Column existence checks were already in place for all 7 types before this PR.BigNumberChartPlugin.post_map_validate().isinstance(config, XYChartConfig)hardcoding fromRuntimeValidator; XY-specific format/cardinality checks now live inXYChartPlugin.get_runtime_warnings().generate_chart.pylisted only'xy'and'table'as valid chart types; updated to list all 7.pie,pivot_table,mixed_timeseriescases for discriminated-union parse failures; hints now come from each plugin'sschema_error_hint()instead of a hardcoded lookup table in the validator.app.pyget a fully populated registry.BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
N/A — backend-only change, no UI modifications.
TESTING INSTRUCTIONS
Manual verification:
python -m superset.mcp_service.servercolumn_not_founderror with fuzzy suggestions (previously: silent pass, runtime failure)big_numberchart withshow_trendline=truebut notemporal_column→ should error at validationgenerate_chartfor all 7 chart types to confirm no regressionsADDITIONAL INFORMATION
Behavioral change:
generate_chartandupdate_chartnow explicitly reject anychart_typevalue that does not map to one of the 7 registered plugins (xy,table,pie,pivot_table,mixed_timeseries,handlebars,big_number). Previously,unknown types fell through various
if/elifchains and could reach later stages silently.This tightening is intentional — the registry is the authoritative list of supported
types, and callers should receive a clear
unknown_chart_typeerror rather thanundefined behaviour.
Column-name sanitization and PR #39915: This branch pre-dates PR #39915
(the dedicated column-regex relaxation PR). The registry refactor carries forward the
same intended security boundary:
ColumnRef.name,FilterConfig.column, andBigNumberChartConfig.temporal_columnno longer use the overly strict ASCII regex,but each field is sanitized at the schema boundary with SQL-pattern checks enabled.
This preserves support for real-world column names such as
1Q_revenue,order-date, andevents.created-atwhile still rejecting SQL-injection patterns.Behavioral change — 5-type column validation gap: There is no separate
Shortcut/GitHub issue for this bug; it was found during the registry refactor. The
previous behavior was inconsistent across chart types:
xyandtablevalidatedcolumn references, while
pie,pivot_table,mixed_timeseries,handlebars, andbig_numbercould accept non-existent columns and fail later at query/runtime. ThisPR intentionally makes validation consistent for all supported MCP chart types.
Scope note: The branch has been rebased onto latest
masterand de-scoped — anearlier revision carried ~25 files of unrelated formatting/typing churn (advanced data
type plugins, various
api.pyfiles, unrelated tests) which have been restored tomatch
master, so the diff is limited tosuperset/mcp_service/, its tests, and theboth wiring points are in
flask_singleton.py.New plugins can be added at any time by creating a file in
superset/mcp_service/chart/plugins/and importing it insuperset/mcp_service/chart/plugins/__init__.py.