You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by "aminghadersohi (via GitHub)" <gi...@apache.org> on 2025/10/25 00:20:52 UTC
[PR] feat(mcp): PR6 - add instance info tool, prompts, and resources [superset]
aminghadersohi opened a new pull request, #35839:
URL: https://github.com/apache/superset/pull/35839
## SUMMARY
This PR completes the MCP service implementation by adding instance-level metadata and AI guidance features. It introduces a comprehensive instance information tool, system prompts for user onboarding, and resource templates for chart configurations.
**New Features Added:**
**Tools (1):**
- **get_superset_instance_info**: Comprehensive instance statistics including dashboards, charts, datasets, activity metrics, database breakdowns, and popular content
**Prompts (2):**
- **superset_quickstart**: Interactive onboarding guide tailored by user type (analyst, executive, developer) and focus area
- **create_chart_guided**: AI-powered chart creation workflow that guides users through business context and visualization selection
**Resources (2):**
- **chart_configs**: Example chart configuration templates for common visualization types (bar, line, pie, scatter, etc.)
- **instance_metadata**: Real-time instance metadata and statistics accessible without tool calls
**Infrastructure Added:**
- **InstanceInfoCore class** (173 lines in mcp_core.py):
- Configurable base class for comprehensive instance reporting
- Custom metric calculators via dependency injection
- Flexible breakdown calculations for dashboards, databases, and popular content
- Extensible design for adding new metrics without modifying core logic
- **System schemas** (85 lines added to system/schemas.py):
- `GetSupersetInstanceInfoRequest` - Request schema with optional metric filters
- `InstanceInfo` - Complete instance information response
- `InstanceSummary` - High-level counts and totals
- `RecentActivity` - User activity metrics (last 7/30 days)
- `DashboardBreakdown` - Dashboard categorization (published vs draft)
- `DatabaseBreakdown` - Database usage statistics
- `PopularContent` - Most viewed/edited content
**Statistics:**
- **Files Changed**: 16 (16 new, 2 modified)
- **Lines Added**: ~1,461
- get_superset_instance_info.py: 268 lines
- InstanceInfoCore in mcp_core.py: 173 lines
- Prompts: 289 lines (quickstart: 94, create_chart_guided: 195)
- Resources: 469 lines (chart_configs: 362, instance_metadata: 107)
- Schemas: 85 lines (7 new schema classes)
- Placeholder __init__.py files: 126 lines (dashboard/dataset prompts/resources)
- **All pre-commit hooks passing**
**Builds on:**
- PR #35163 (MCP service scaffold)
- PR2 (chart listing and info tools)
- PR3 (dashboard and dataset listing and info tools)
- PR4 (advanced chart tools and SQL Lab integration)
- PR5 (dashboard generation and chart placement tools)
## BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
N/A - This is a backend API enhancement for the MCP service.
## TESTING INSTRUCTIONS
### Prerequisites
1. Ensure you have a running Superset instance with the MCP service enabled
2. Ensure you have the development environment set up (Python venv activated)
3. Have some dashboards, charts, and datasets created in your Superset instance for meaningful statistics
### Manual Testing with MCP Client
If you have an MCP client configured (Claude Desktop, Cline, etc.), you can test the new features:
#### 1. Test Instance Info Tool
```python
# Via MCP client: Request instance information
Use the get_superset_instance_info tool
# Expected response includes:
# - Summary counts (total dashboards, charts, datasets, databases)
# - Recent activity (user counts, chart views, edits)
# - Dashboard breakdown (published vs draft)
# - Database breakdown (usage by database)
# - Popular content (most viewed/edited items)
```
#### 2. Test Prompts
```
# Via MCP client: Access the quickstart prompt
Request the "superset_quickstart" prompt with:
user_type: "analyst"
focus_area: "sales"
# Expected: Personalized onboarding guide with:
# - Welcome message tailored to analysts
# - Sales-focused example queries
# - Step-by-step visualization creation
# - Dashboard building guidance
# Try the chart creation prompt
Request the "create_chart_guided" prompt
# Expected: Interactive chart creation guide with:
# - Business context questions
# - Available dataset suggestions
# - Chart type recommendations
# - Configuration examples
```
#### 3. Test Resources
```
# Via MCP client: Access chart configuration examples
Request the "chart_configs" resource
# Expected: Template configurations for:
# - Bar charts
# - Line charts
# - Pie charts
# - Scatter plots
# - Time series
# And more...
# Request instance metadata
Request the "instance_metadata" resource
# Expected: Current instance stats including:
# - Available databases
# - Dataset count
# - Chart type distribution
# - User activity
```
### Pre-Commit Validation
```bash
# Stage all files
git add -A
# Run pre-commit hooks on changed files
pre-commit run --files \
superset/mcp_service/system/tool/get_superset_instance_info.py \
superset/mcp_service/system/prompts/quickstart.py \
superset/mcp_service/chart/prompts/create_chart_guided.py \
superset/mcp_service/chart/resources/chart_configs.py \
superset/mcp_service/system/resources/instance_metadata.py \
superset/mcp_service/mcp_core.py \
superset/mcp_service/system/schemas.py
# Expected: All hooks pass (mypy, ruff, pylint, etc.)
```
### Verification Checklist
- [ ] Instance info tool returns valid statistics
- [ ] Quickstart prompt generates personalized guidance
- [ ] Create chart prompt provides helpful workflow
- [ ] Chart configs resource shows template examples
- [ ] Instance metadata resource shows current stats
- [ ] All schemas validate correctly
- [ ] InstanceInfoCore class allows custom metric calculators
- [ ] Pre-commit hooks pass
## ADDITIONAL INFORMATION
- [ ] Has associated issue: No
- [ ] Required feature flags: No
- [ ] Changes UI: No
- [ ] Includes DB Migration: No
- [x] Introduces new feature or API: Yes (1 tool, 2 prompts, 2 resources)
- [ ] Removes existing feature or API: No
**Completes MCP Service Implementation:**
This PR represents the final phase of the core MCP service implementation, adding:
1. **Instance-level introspection** - AI agents can understand what data and content is available
2. **User guidance** - Prompts help AI provide better user experiences
3. **Reference templates** - Resources provide examples for common use cases
**Files Changed Summary:**
**New Tool (1 file):**
- superset/mcp_service/system/tool/get_superset_instance_info.py (268 lines)
**New Prompts (3 files):**
- superset/mcp_service/system/prompts/__init__.py (21 lines)
- superset/mcp_service/system/prompts/quickstart.py (94 lines)
- superset/mcp_service/chart/prompts/__init__.py (21 lines)
- superset/mcp_service/chart/prompts/create_chart_guided.py (195 lines)
**New Resources (3 files):**
- superset/mcp_service/system/resources/__init__.py (21 lines)
- superset/mcp_service/system/resources/instance_metadata.py (107 lines)
- superset/mcp_service/chart/resources/__init__.py (21 lines)
- superset/mcp_service/chart/resources/chart_configs.py (362 lines)
**Placeholder Files (4 files):**
- superset/mcp_service/dashboard/prompts/__init__.py (21 lines)
- superset/mcp_service/dashboard/resources/__init__.py (21 lines)
- superset/mcp_service/dataset/prompts/__init__.py (21 lines)
- superset/mcp_service/dataset/resources/__init__.py (21 lines)
**Updated Files (2 files):**
- superset/mcp_service/mcp_core.py (+173 lines for InstanceInfoCore)
- superset/mcp_service/system/schemas.py (+85 lines, 7 new schemas)
- superset/mcp_service/system/tool/__init__.py (export new tool)
**Key Features:**
1. **get_superset_instance_info**:
- Comprehensive instance statistics
- Dashboard breakdown (published vs draft)
- Database usage metrics
- Recent activity tracking (7 and 30 days)
- Popular content identification
- Configurable via InstanceInfoCore base class
2. **superset_quickstart prompt**:
- Personalized by user type (analyst, executive, developer)
- Focused by area of interest (sales, marketing, operations, general)
- Interactive onboarding workflow
- Dataset discovery guidance
- First visualization creation
- Dashboard building basics
3. **create_chart_guided prompt**:
- Business context gathering
- Dataset recommendation
- Chart type suggestion based on data and goals
- Configuration examples
- Best practices guidance
4. **chart_configs resource**:
- Template configurations for common chart types
- Example form_data structures
- Visualization-specific options
- Quick reference for AI agents
5. **instance_metadata resource**:
- Real-time instance statistics
- Available database connections
- Dataset summaries
- User activity metrics
- No tool call required (static resource)
**Architecture Highlights:**
The **InstanceInfoCore** class uses dependency injection for metric calculation, allowing custom implementations:
```python
# Default implementation
instance_info_core = InstanceInfoCore()
info = instance_info_core.get_instance_info()
# Custom metric calculators
custom_core = InstanceInfoCore(
dashboard_calculator=my_custom_dashboard_metrics,
database_calculator=my_custom_db_metrics
)
```
This design enables:
- Easy testing with mock calculators
- Custom metrics for different deployment scenarios
- Extension without modifying core logic
- Separation of concerns between data access and formatting
**Pattern Compliance:**
This PR continues the **MINIMAL cherry-pick pattern** established in previous PRs:
- ✅ Copied ONLY the required tool, prompt, and resource files
- ✅ NO development utilities or extra infrastructure
- ✅ Updated exports in `__init__.py` files
- ✅ Added comprehensive schemas for type safety
- ✅ All pre-commit hooks passing
- ✅ Clean separation of concerns
**Next Steps:**
The MCP service is now feature-complete for the initial release. Future enhancements may include:
- Additional prompts for dashboard and dataset modules
- Custom resources for database-specific examples
- Advanced instance metrics (query performance, cache hit rates)
- Multi-tenant instance information
- Prompt testing framework for validating LLM guidance quality
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(mcp): PR6 - add instance info tool, prompts, and resources [superset]
Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi commented on PR #35839:
URL: https://github.com/apache/superset/pull/35839#issuecomment-3470885098
closing as this stack is too hard to maintain. i am just gonna keep the last one. PR10
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(mcp): PR6 - add instance info tool, prompts, and resources [superset]
Posted by "korbit-ai[bot] (via GitHub)" <gi...@apache.org>.
korbit-ai[bot] commented on PR #35839:
URL: https://github.com/apache/superset/pull/35839#issuecomment-3445305805
Korbit doesn't automatically review large (3000+ lines changed) pull requests such as this one. If you want me to review anyway, use `/korbit-review`.
[](f546f235-b9ca-44ac-bac4-62ec051af147)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(mcp): PR6 - add instance info tool, prompts, and resources [superset]
Posted by "github-advanced-security[bot] (via GitHub)" <gi...@apache.org>.
github-advanced-security[bot] commented on code in PR #35839:
URL: https://github.com/apache/superset/pull/35839#discussion_r2462291100
##########
tests/unit_tests/mcp_service/chart/test_chart_utils.py:
##########
@@ -0,0 +1,460 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Tests for chart utilities module"""
+
+from unittest.mock import patch
+
+import pytest
+
+from superset.mcp_service.chart.chart_utils import (
+ create_metric_object,
+ generate_chart_name,
+ generate_explore_link,
+ map_config_to_form_data,
+ map_filter_operator,
+ map_table_config,
+ map_xy_config,
+)
+from superset.mcp_service.chart.schemas import (
+ AxisConfig,
+ ColumnRef,
+ FilterConfig,
+ LegendConfig,
+ TableChartConfig,
+ XYChartConfig,
+)
+
+
+class TestCreateMetricObject:
+ """Test create_metric_object function"""
+
+ def test_create_metric_object_with_aggregate(self) -> None:
+ """Test creating metric object with specified aggregate"""
+ col = ColumnRef(name="revenue", aggregate="SUM", label="Total Revenue")
+ result = create_metric_object(col)
+
+ assert result["aggregate"] == "SUM"
+ assert result["column"]["column_name"] == "revenue"
+ assert result["label"] == "Total Revenue"
+ assert result["optionName"] == "metric_revenue"
+ assert result["expressionType"] == "SIMPLE"
+
+ def test_create_metric_object_default_aggregate(self) -> None:
+ """Test creating metric object with default aggregate"""
+ col = ColumnRef(name="orders")
+ result = create_metric_object(col)
+
+ assert result["aggregate"] == "SUM"
+ assert result["column"]["column_name"] == "orders"
+ assert result["label"] == "SUM(orders)"
+ assert result["optionName"] == "metric_orders"
+
+
+class TestMapFilterOperator:
+ """Test map_filter_operator function"""
+
+ def test_map_filter_operators(self) -> None:
+ """Test mapping of various filter operators"""
+ assert map_filter_operator("=") == "=="
+ assert map_filter_operator(">") == ">"
+ assert map_filter_operator("<") == "<"
+ assert map_filter_operator(">=") == ">="
+ assert map_filter_operator("<=") == "<="
+ assert map_filter_operator("!=") == "!="
+
+ def test_map_filter_operator_unknown(self) -> None:
+ """Test mapping of unknown operator returns original"""
+ assert map_filter_operator("UNKNOWN") == "UNKNOWN"
+
+
+class TestMapTableConfig:
+ """Test map_table_config function"""
+
+ def test_map_table_config_basic(self) -> None:
+ """Test basic table config mapping with aggregated columns"""
+ config = TableChartConfig(
+ columns=[
+ ColumnRef(name="product", aggregate="COUNT"),
+ ColumnRef(name="revenue", aggregate="SUM"),
+ ]
+ )
+
+ result = map_table_config(config)
+
+ assert result["viz_type"] == "table"
+ assert result["query_mode"] == "aggregate"
+ # Aggregated columns should be in metrics, not all_columns
+ assert "all_columns" not in result
+ assert len(result["metrics"]) == 2
+ assert result["metrics"][0]["aggregate"] == "COUNT"
+ assert result["metrics"][1]["aggregate"] == "SUM"
+
+ def test_map_table_config_raw_columns(self) -> None:
+ """Test table config mapping with raw columns (no aggregates)"""
+ config = TableChartConfig(
+ columns=[
+ ColumnRef(name="product"),
+ ColumnRef(name="category"),
+ ]
+ )
+
+ result = map_table_config(config)
+
+ assert result["viz_type"] == "table"
+ assert result["query_mode"] == "raw"
+ # Raw columns should be in all_columns
+ assert result["all_columns"] == ["product", "category"]
+ assert "metrics" not in result
+
+ def test_map_table_config_with_filters(self) -> None:
+ """Test table config mapping with filters"""
+ config = TableChartConfig(
+ columns=[ColumnRef(name="product")],
+ filters=[FilterConfig(column="status", op="=", value="active")],
+ )
+
+ result = map_table_config(config)
+
+ assert "adhoc_filters" in result
+ assert len(result["adhoc_filters"]) == 1
+ filter_obj = result["adhoc_filters"][0]
+ assert filter_obj["subject"] == "status"
+ assert filter_obj["operator"] == "=="
+ assert filter_obj["comparator"] == "active"
+ assert filter_obj["expressionType"] == "SIMPLE"
+
+ def test_map_table_config_with_sort(self) -> None:
+ """Test table config mapping with sort"""
+ config = TableChartConfig(
+ columns=[ColumnRef(name="product")], sort_by=["product", "revenue"]
+ )
+
+ result = map_table_config(config)
+ assert result["order_by_cols"] == ["product", "revenue"]
+
+
+class TestMapXYConfig:
+ """Test map_xy_config function"""
+
+ def test_map_xy_config_line_chart(self) -> None:
+ """Test XY config mapping for line chart"""
+ config = XYChartConfig(
+ x=ColumnRef(name="date"),
+ y=[ColumnRef(name="revenue", aggregate="SUM")],
+ kind="line",
+ )
+
+ result = map_xy_config(config)
+
+ assert result["viz_type"] == "echarts_timeseries_line"
+ assert result["x_axis"] == "date"
+ assert len(result["metrics"]) == 1
+ assert result["metrics"][0]["aggregate"] == "SUM"
+
+ def test_map_xy_config_with_groupby(self) -> None:
+ """Test XY config mapping with group by"""
+ config = XYChartConfig(
+ x=ColumnRef(name="date"),
+ y=[ColumnRef(name="revenue")],
+ kind="bar",
+ group_by=ColumnRef(name="region"),
+ )
+
+ result = map_xy_config(config)
+
+ assert result["viz_type"] == "echarts_timeseries_bar"
+ assert result["groupby"] == ["region"]
+
+ def test_map_xy_config_with_axes(self) -> None:
+ """Test XY config mapping with axis configurations"""
+ config = XYChartConfig(
+ x=ColumnRef(name="date"),
+ y=[ColumnRef(name="revenue")],
+ kind="area",
+ x_axis=AxisConfig(title="Date", format="%Y-%m-%d"),
+ y_axis=AxisConfig(title="Revenue", scale="log", format="$,.2f"),
+ )
+
+ result = map_xy_config(config)
+
+ assert result["viz_type"] == "echarts_area"
+ assert result["x_axis_title"] == "Date"
+ assert result["x_axis_format"] == "%Y-%m-%d"
+ assert result["y_axis_title"] == "Revenue"
+ assert result["y_axis_format"] == "$,.2f"
+ assert result["y_axis_scale"] == "log"
+
+ def test_map_xy_config_with_legend(self) -> None:
+ """Test XY config mapping with legend configuration"""
+ config = XYChartConfig(
+ x=ColumnRef(name="date"),
+ y=[ColumnRef(name="revenue")],
+ kind="scatter",
+ legend=LegendConfig(show=False, position="top"),
+ )
+
+ result = map_xy_config(config)
+
+ assert result["viz_type"] == "echarts_timeseries_scatter"
+ assert result["show_legend"] is False
+ assert result["legend_orientation"] == "top"
+
+
+class TestMapConfigToFormData:
+ """Test map_config_to_form_data function"""
+
+ def test_map_table_config_type(self) -> None:
+ """Test mapping table config type"""
+ config = TableChartConfig(columns=[ColumnRef(name="test")])
+ result = map_config_to_form_data(config)
+ assert result["viz_type"] == "table"
+
+ def test_map_xy_config_type(self) -> None:
+ """Test mapping XY config type"""
+ config = XYChartConfig(
+ x=ColumnRef(name="date"), y=[ColumnRef(name="revenue")], kind="line"
+ )
+ result = map_config_to_form_data(config)
+ assert result["viz_type"] == "echarts_timeseries_line"
+
+ def test_map_unsupported_config_type(self) -> None:
+ """Test mapping unsupported config type raises error"""
+ with pytest.raises(ValueError, match="Unsupported config type"):
+ map_config_to_form_data("invalid_config") # type: ignore
+
+
+class TestGenerateChartName:
+ """Test generate_chart_name function"""
+
+ def test_generate_table_chart_name(self) -> None:
+ """Test generating name for table chart"""
+ config = TableChartConfig(
+ columns=[
+ ColumnRef(name="product"),
+ ColumnRef(name="revenue"),
+ ]
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Table Chart - product, revenue"
+
+ def test_generate_xy_chart_name(self) -> None:
+ """Test generating name for XY chart"""
+ config = XYChartConfig(
+ x=ColumnRef(name="date"),
+ y=[ColumnRef(name="revenue"), ColumnRef(name="orders")],
+ kind="line",
+ )
+
+ result = generate_chart_name(config)
+ assert result == "Line Chart - date vs revenue, orders"
+
+ def test_generate_chart_name_unsupported(self) -> None:
+ """Test generating name for unsupported config type"""
+ result = generate_chart_name("invalid_config") # type: ignore
+ assert result == "Chart"
+
+
+class TestGenerateExploreLink:
+ """Test generate_explore_link function"""
+
+ @patch("superset.mcp_service.chart.chart_utils.get_superset_base_url")
+ def test_generate_explore_link_uses_base_url(self, mock_get_base_url) -> None:
+ """Test that generate_explore_link uses the configured base URL"""
+ mock_get_base_url.return_value = "https://superset.example.com"
+ form_data = {"viz_type": "table", "metrics": ["count"]}
+
+ result = generate_explore_link("123", form_data)
+
+ # Should use the configured base URL
+ assert result.startswith("https://superset.example.com")
Review Comment:
## Incomplete URL substring sanitization
The string [https://superset.example.com](1) may be at an arbitrary position in the sanitized URL.
[Show more details](https://github.com/apache/superset/security/code-scanning/2064)
##########
superset/mcp_service/chart/schemas.py:
##########
@@ -0,0 +1,1075 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Pydantic schemas for chart-related responses
+"""
+
+from __future__ import annotations
+
+import html
+import re
+from datetime import datetime, timezone
+from typing import Annotated, Any, Dict, List, Literal, Protocol
+
+from pydantic import (
+ BaseModel,
+ ConfigDict,
+ Field,
+ field_validator,
+ model_validator,
+ PositiveInt,
+)
+
+from superset.daos.base import ColumnOperator, ColumnOperatorEnum
+from superset.mcp_service.common.cache_schemas import (
+ CacheStatus,
+ FormDataCacheControl,
+ MetadataCacheControl,
+ QueryCacheControl,
+)
+from superset.mcp_service.system.schemas import (
+ PaginationInfo,
+ TagInfo,
+ UserInfo,
+)
+
+
+class ChartLike(Protocol):
+ """Protocol for chart-like objects with expected attributes."""
+
+ id: int
+ slice_name: str | None
+ viz_type: str | None
+ datasource_name: str | None
+ datasource_type: str | None
+ url: str | None
+ description: str | None
+ cache_timeout: int | None
+ form_data: Dict[str, Any] | None
+ query_context: Any | None
+ changed_by: Any | None # User object
+ changed_by_name: str | None
+ changed_on: str | datetime | None
+ changed_on_humanized: str | None
+ created_by: Any | None # User object
+ created_by_name: str | None
+ created_on: str | datetime | None
+ created_on_humanized: str | None
+ uuid: str | None
+ tags: List[Any] | None
+ owners: List[Any] | None
+
+
+class ChartInfo(BaseModel):
+ """Full chart model with all possible attributes."""
+
+ id: int = Field(..., description="Chart ID")
+ slice_name: str = Field(..., description="Chart name")
+ viz_type: str | None = Field(None, description="Visualization type")
+ datasource_name: str | None = Field(None, description="Datasource name")
+ datasource_type: str | None = Field(None, description="Datasource type")
+ url: str | None = Field(None, description="Chart URL")
+ description: str | None = Field(None, description="Chart description")
+ cache_timeout: int | None = Field(None, description="Cache timeout")
+ form_data: Dict[str, Any] | None = Field(None, description="Chart form data")
+ query_context: Any | None = Field(None, description="Query context")
+ changed_by: str | None = Field(None, description="Last modifier (username)")
+ changed_by_name: str | None = Field(
+ None, description="Last modifier (display name)"
+ )
+ changed_on: str | datetime | None = Field(
+ None, description="Last modification timestamp"
+ )
+ changed_on_humanized: str | None = Field(
+ None, description="Humanized modification time"
+ )
+ created_by: str | None = Field(None, description="Chart creator (username)")
+ created_on: str | datetime | None = Field(None, description="Creation timestamp")
+ created_on_humanized: str | None = Field(
+ None, description="Humanized creation time"
+ )
+ uuid: str | None = Field(None, description="Chart UUID")
+ tags: List[TagInfo] = Field(default_factory=list, description="Chart tags")
+ owners: List[UserInfo] = Field(default_factory=list, description="Chart owners")
+ model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
+
+
+class GetChartAvailableFiltersRequest(BaseModel):
+ """
+ Request schema for get_chart_available_filters tool.
+
+ Currently has no parameters but provides consistent API for future extensibility.
+ """
+
+ model_config = ConfigDict(
+ extra="forbid",
+ str_strip_whitespace=True,
+ )
+
+
+class ChartAvailableFiltersResponse(BaseModel):
+ column_operators: Dict[str, Any] = Field(
+ ..., description="Available filter operators and metadata for each column"
+ )
+
+
+class ChartError(BaseModel):
+ error: str = Field(..., description="Error message")
+ error_type: str = Field(..., description="Type of error")
+ timestamp: datetime = Field(
+ default_factory=lambda: datetime.now(timezone.utc),
+ description="Error timestamp",
+ )
+ model_config = ConfigDict(ser_json_timedelta="iso8601")
+
+
+class ChartCapabilities(BaseModel):
+ """Describes what the chart can do for LLM understanding."""
+
+ supports_interaction: bool = Field(description="Chart supports user interaction")
+ supports_real_time: bool = Field(description="Chart supports live data updates")
+ supports_drill_down: bool = Field(
+ description="Chart supports drill-down navigation"
+ )
+ supports_export: bool = Field(description="Chart can be exported to other formats")
+ optimal_formats: List[str] = Field(description="Recommended preview formats")
+ data_types: List[str] = Field(
+ description="Types of data shown (time_series, categorical, etc)"
+ )
+
+
+class ChartSemantics(BaseModel):
+ """Semantic information for LLM reasoning."""
+
+ primary_insight: str = Field(
+ description="Main insight or pattern the chart reveals"
+ )
+ data_story: str = Field(description="Narrative description of what the data shows")
+ recommended_actions: List[str] = Field(
+ description="Suggested next steps based on data"
+ )
+ anomalies: List[str] = Field(description="Notable outliers or unusual patterns")
+ statistical_summary: Dict[str, Any] = Field(
+ description="Key statistics (mean, median, trends)"
+ )
+
+
+class PerformanceMetadata(BaseModel):
+ """Performance information for LLM cost understanding."""
+
+ query_duration_ms: int = Field(description="Query execution time")
+ estimated_cost: str | None = Field(None, description="Resource cost estimate")
+ cache_status: str = Field(description="Cache hit/miss status")
+ optimization_suggestions: List[str] = Field(
+ default_factory=list, description="Performance improvement tips"
+ )
+
+
+class AccessibilityMetadata(BaseModel):
+ """Accessibility information for inclusive visualization."""
+
+ color_blind_safe: bool = Field(description="Uses colorblind-safe palette")
+ alt_text: str = Field(description="Screen reader description")
+ high_contrast_available: bool = Field(description="High contrast version available")
+
+
+class VersionedResponse(BaseModel):
+ """Base class for versioned API responses."""
+
+ schema_version: str = Field("2.0", description="Response schema version")
+ api_version: str = Field("v1", description="MCP API version")
+
+
+class GetChartInfoRequest(BaseModel):
+ """Request schema for get_chart_info with support for ID or UUID."""
+
+ identifier: Annotated[
+ int | str,
+ Field(description="Chart identifier - can be numeric ID or UUID string"),
+ ]
+
+
+def serialize_chart_object(chart: ChartLike | None) -> ChartInfo | None:
+ if not chart:
+ return None
+
+ # Generate MCP service screenshot URL instead of chart's native URL
+ from superset.mcp_service.utils.url_utils import get_chart_screenshot_url
+
+ chart_id = getattr(chart, "id", None)
+ screenshot_url = None
+ if chart_id:
+ screenshot_url = get_chart_screenshot_url(chart_id)
+
+ return ChartInfo(
+ id=chart_id,
+ slice_name=getattr(chart, "slice_name", None),
+ viz_type=getattr(chart, "viz_type", None),
+ datasource_name=getattr(chart, "datasource_name", None),
+ datasource_type=getattr(chart, "datasource_type", None),
+ url=screenshot_url,
+ description=getattr(chart, "description", None),
+ cache_timeout=getattr(chart, "cache_timeout", None),
+ form_data=getattr(chart, "form_data", None),
+ query_context=getattr(chart, "query_context", None),
+ changed_by=getattr(chart, "changed_by_name", None)
+ or (str(chart.changed_by) if getattr(chart, "changed_by", None) else None),
+ changed_by_name=getattr(chart, "changed_by_name", None),
+ changed_on=getattr(chart, "changed_on", None),
+ changed_on_humanized=getattr(chart, "changed_on_humanized", None),
+ created_by=getattr(chart, "created_by_name", None)
+ or (str(chart.created_by) if getattr(chart, "created_by", None) else None),
+ created_on=getattr(chart, "created_on", None),
+ created_on_humanized=getattr(chart, "created_on_humanized", None),
+ uuid=str(getattr(chart, "uuid", "")) if getattr(chart, "uuid", None) else None,
+ tags=[
+ TagInfo.model_validate(tag, from_attributes=True)
+ for tag in getattr(chart, "tags", [])
+ ]
+ if getattr(chart, "tags", None)
+ else [],
+ owners=[
+ UserInfo.model_validate(owner, from_attributes=True)
+ for owner in getattr(chart, "owners", [])
+ ]
+ if getattr(chart, "owners", None)
+ else [],
+ )
+
+
+class ChartFilter(ColumnOperator):
+ """
+ Filter object for chart listing.
+ col: The column to filter on. Must be one of the allowed filter fields.
+ opr: The operator to use. Must be one of the supported operators.
+ value: The value to filter by (type depends on col and opr).
+ """
+
+ col: Literal[
+ "slice_name",
+ "viz_type",
+ "datasource_name",
+ ] = Field(
+ ...,
+ description="Column to filter on. See get_chart_available_filters for "
+ "allowed values.",
+ )
+ opr: ColumnOperatorEnum = Field(
+ ...,
+ description="Operator to use. See get_chart_available_filters for "
+ "allowed values.",
+ )
+ value: str | int | float | bool | List[str | int | float | bool] = Field(
+ ..., description="Value to filter by (type depends on col and opr)"
+ )
+
+
+class ChartList(BaseModel):
+ charts: List[ChartInfo]
+ count: int
+ total_count: int
+ page: int
+ page_size: int
+ total_pages: int
+ has_previous: bool
+ has_next: bool
+ columns_requested: List[str] | None = None
+ columns_loaded: List[str] | None = None
+ filters_applied: List[ChartFilter] = Field(
+ default_factory=list,
+ description="List of advanced filter dicts applied to the query.",
+ )
+ pagination: PaginationInfo | None = None
+ timestamp: datetime | None = None
+ model_config = ConfigDict(ser_json_timedelta="iso8601")
+
+
+# --- Simplified schemas for generate_chart tool ---
+
+
+# Common pieces
+class ColumnRef(BaseModel):
+ name: str = Field(
+ ...,
+ description="Column name",
+ min_length=1,
+ max_length=255,
+ pattern=r"^[a-zA-Z0-9_][a-zA-Z0-9_\s\-\.]*$",
+ )
+ label: str | None = Field(
+ None, description="Display label for the column", max_length=500
+ )
+ dtype: str | None = Field(None, description="Data type hint")
+ aggregate: (
+ Literal[
+ "SUM",
+ "COUNT",
+ "AVG",
+ "MIN",
+ "MAX",
+ "COUNT_DISTINCT",
+ "STDDEV",
+ "VAR",
+ "MEDIAN",
+ "PERCENTILE",
+ ]
+ | None
+ ) = Field(
+ None,
+ description="SQL aggregation function. Only these validated functions are "
+ "supported to prevent SQL errors.",
+ )
+
+ @field_validator("name")
+ @classmethod
+ def sanitize_name(cls, v: str) -> str:
+ """Sanitize column name to prevent XSS and SQL injection."""
+ if not v or not v.strip():
+ raise ValueError("Column name cannot be empty")
+
+ # Remove HTML tags and decode entities
+ sanitized = html.escape(v.strip())
+
+ # Check for script content
+ if re.search(r"<script[^>]*>.*?</script>", v, re.IGNORECASE | re.DOTALL):
+ raise ValueError(
+ "Column name contains potentially malicious script content"
+ )
+
+ # Basic SQL injection patterns (basic protection)
+ dangerous_patterns = [
+ r"(;|\||&|\$|`)",
+ r"\b(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER|EXEC|EXECUTE)\b",
+ r"--",
+ r"/\*.*\*/",
+ ]
+
+ for pattern in dangerous_patterns:
+ if re.search(pattern, v, re.IGNORECASE):
+ raise ValueError(
+ "Column name contains potentially unsafe characters or SQL keywords"
+ )
+
+ return sanitized
+
+ @field_validator("label")
+ @classmethod
+ def sanitize_label(cls, v: str | None) -> str | None:
+ """Sanitize display label to prevent XSS attacks."""
+ if v is None:
+ return v
+
+ # Strip whitespace
+ v = v.strip()
+ if not v:
+ return None
+
+ # Check for dangerous HTML tags and JavaScript protocols BEFORE escaping
+ dangerous_patterns = [
+ r"<script[^>]*>.*?</script>", # Script tags
+ r"<iframe[^>]*>.*?</iframe>", # Iframe tags
+ r"<object[^>]*>.*?</object>", # Object tags
+ r"<embed[^>]*>.*?</embed>", # Embed tags
+ r"<link[^>]*>", # Link tags
+ r"<meta[^>]*>", # Meta tags
+ r"javascript:", # JavaScript protocol
+ r"vbscript:", # VBScript protocol
+ r"data:text/html", # Data URL HTML
+ r"on\w+\s*=", # Event handlers (onclick, onload, etc)
+ ]
+
+ for pattern in dangerous_patterns:
+ if re.search(pattern, v, re.IGNORECASE | re.DOTALL):
+ raise ValueError(
+ "Label contains potentially malicious content. "
Review Comment:
## Bad HTML filtering regexp
This regular expression does not match script end tags like </script >.
[Show more details](https://github.com/apache/superset/security/code-scanning/2044)
##########
superset/mcp_service/chart/schemas.py:
##########
@@ -0,0 +1,1075 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+Pydantic schemas for chart-related responses
+"""
+
+from __future__ import annotations
+
+import html
+import re
+from datetime import datetime, timezone
+from typing import Annotated, Any, Dict, List, Literal, Protocol
+
+from pydantic import (
+ BaseModel,
+ ConfigDict,
+ Field,
+ field_validator,
+ model_validator,
+ PositiveInt,
+)
+
+from superset.daos.base import ColumnOperator, ColumnOperatorEnum
+from superset.mcp_service.common.cache_schemas import (
+ CacheStatus,
+ FormDataCacheControl,
+ MetadataCacheControl,
+ QueryCacheControl,
+)
+from superset.mcp_service.system.schemas import (
+ PaginationInfo,
+ TagInfo,
+ UserInfo,
+)
+
+
+class ChartLike(Protocol):
+ """Protocol for chart-like objects with expected attributes."""
+
+ id: int
+ slice_name: str | None
+ viz_type: str | None
+ datasource_name: str | None
+ datasource_type: str | None
+ url: str | None
+ description: str | None
+ cache_timeout: int | None
+ form_data: Dict[str, Any] | None
+ query_context: Any | None
+ changed_by: Any | None # User object
+ changed_by_name: str | None
+ changed_on: str | datetime | None
+ changed_on_humanized: str | None
+ created_by: Any | None # User object
+ created_by_name: str | None
+ created_on: str | datetime | None
+ created_on_humanized: str | None
+ uuid: str | None
+ tags: List[Any] | None
+ owners: List[Any] | None
+
+
+class ChartInfo(BaseModel):
+ """Full chart model with all possible attributes."""
+
+ id: int = Field(..., description="Chart ID")
+ slice_name: str = Field(..., description="Chart name")
+ viz_type: str | None = Field(None, description="Visualization type")
+ datasource_name: str | None = Field(None, description="Datasource name")
+ datasource_type: str | None = Field(None, description="Datasource type")
+ url: str | None = Field(None, description="Chart URL")
+ description: str | None = Field(None, description="Chart description")
+ cache_timeout: int | None = Field(None, description="Cache timeout")
+ form_data: Dict[str, Any] | None = Field(None, description="Chart form data")
+ query_context: Any | None = Field(None, description="Query context")
+ changed_by: str | None = Field(None, description="Last modifier (username)")
+ changed_by_name: str | None = Field(
+ None, description="Last modifier (display name)"
+ )
+ changed_on: str | datetime | None = Field(
+ None, description="Last modification timestamp"
+ )
+ changed_on_humanized: str | None = Field(
+ None, description="Humanized modification time"
+ )
+ created_by: str | None = Field(None, description="Chart creator (username)")
+ created_on: str | datetime | None = Field(None, description="Creation timestamp")
+ created_on_humanized: str | None = Field(
+ None, description="Humanized creation time"
+ )
+ uuid: str | None = Field(None, description="Chart UUID")
+ tags: List[TagInfo] = Field(default_factory=list, description="Chart tags")
+ owners: List[UserInfo] = Field(default_factory=list, description="Chart owners")
+ model_config = ConfigDict(from_attributes=True, ser_json_timedelta="iso8601")
+
+
+class GetChartAvailableFiltersRequest(BaseModel):
+ """
+ Request schema for get_chart_available_filters tool.
+
+ Currently has no parameters but provides consistent API for future extensibility.
+ """
+
+ model_config = ConfigDict(
+ extra="forbid",
+ str_strip_whitespace=True,
+ )
+
+
+class ChartAvailableFiltersResponse(BaseModel):
+ column_operators: Dict[str, Any] = Field(
+ ..., description="Available filter operators and metadata for each column"
+ )
+
+
+class ChartError(BaseModel):
+ error: str = Field(..., description="Error message")
+ error_type: str = Field(..., description="Type of error")
+ timestamp: datetime = Field(
+ default_factory=lambda: datetime.now(timezone.utc),
+ description="Error timestamp",
+ )
+ model_config = ConfigDict(ser_json_timedelta="iso8601")
+
+
+class ChartCapabilities(BaseModel):
+ """Describes what the chart can do for LLM understanding."""
+
+ supports_interaction: bool = Field(description="Chart supports user interaction")
+ supports_real_time: bool = Field(description="Chart supports live data updates")
+ supports_drill_down: bool = Field(
+ description="Chart supports drill-down navigation"
+ )
+ supports_export: bool = Field(description="Chart can be exported to other formats")
+ optimal_formats: List[str] = Field(description="Recommended preview formats")
+ data_types: List[str] = Field(
+ description="Types of data shown (time_series, categorical, etc)"
+ )
+
+
+class ChartSemantics(BaseModel):
+ """Semantic information for LLM reasoning."""
+
+ primary_insight: str = Field(
+ description="Main insight or pattern the chart reveals"
+ )
+ data_story: str = Field(description="Narrative description of what the data shows")
+ recommended_actions: List[str] = Field(
+ description="Suggested next steps based on data"
+ )
+ anomalies: List[str] = Field(description="Notable outliers or unusual patterns")
+ statistical_summary: Dict[str, Any] = Field(
+ description="Key statistics (mean, median, trends)"
+ )
+
+
+class PerformanceMetadata(BaseModel):
+ """Performance information for LLM cost understanding."""
+
+ query_duration_ms: int = Field(description="Query execution time")
+ estimated_cost: str | None = Field(None, description="Resource cost estimate")
+ cache_status: str = Field(description="Cache hit/miss status")
+ optimization_suggestions: List[str] = Field(
+ default_factory=list, description="Performance improvement tips"
+ )
+
+
+class AccessibilityMetadata(BaseModel):
+ """Accessibility information for inclusive visualization."""
+
+ color_blind_safe: bool = Field(description="Uses colorblind-safe palette")
+ alt_text: str = Field(description="Screen reader description")
+ high_contrast_available: bool = Field(description="High contrast version available")
+
+
+class VersionedResponse(BaseModel):
+ """Base class for versioned API responses."""
+
+ schema_version: str = Field("2.0", description="Response schema version")
+ api_version: str = Field("v1", description="MCP API version")
+
+
+class GetChartInfoRequest(BaseModel):
+ """Request schema for get_chart_info with support for ID or UUID."""
+
+ identifier: Annotated[
+ int | str,
+ Field(description="Chart identifier - can be numeric ID or UUID string"),
+ ]
+
+
+def serialize_chart_object(chart: ChartLike | None) -> ChartInfo | None:
+ if not chart:
+ return None
+
+ # Generate MCP service screenshot URL instead of chart's native URL
+ from superset.mcp_service.utils.url_utils import get_chart_screenshot_url
+
+ chart_id = getattr(chart, "id", None)
+ screenshot_url = None
+ if chart_id:
+ screenshot_url = get_chart_screenshot_url(chart_id)
+
+ return ChartInfo(
+ id=chart_id,
+ slice_name=getattr(chart, "slice_name", None),
+ viz_type=getattr(chart, "viz_type", None),
+ datasource_name=getattr(chart, "datasource_name", None),
+ datasource_type=getattr(chart, "datasource_type", None),
+ url=screenshot_url,
+ description=getattr(chart, "description", None),
+ cache_timeout=getattr(chart, "cache_timeout", None),
+ form_data=getattr(chart, "form_data", None),
+ query_context=getattr(chart, "query_context", None),
+ changed_by=getattr(chart, "changed_by_name", None)
+ or (str(chart.changed_by) if getattr(chart, "changed_by", None) else None),
+ changed_by_name=getattr(chart, "changed_by_name", None),
+ changed_on=getattr(chart, "changed_on", None),
+ changed_on_humanized=getattr(chart, "changed_on_humanized", None),
+ created_by=getattr(chart, "created_by_name", None)
+ or (str(chart.created_by) if getattr(chart, "created_by", None) else None),
+ created_on=getattr(chart, "created_on", None),
+ created_on_humanized=getattr(chart, "created_on_humanized", None),
+ uuid=str(getattr(chart, "uuid", "")) if getattr(chart, "uuid", None) else None,
+ tags=[
+ TagInfo.model_validate(tag, from_attributes=True)
+ for tag in getattr(chart, "tags", [])
+ ]
+ if getattr(chart, "tags", None)
+ else [],
+ owners=[
+ UserInfo.model_validate(owner, from_attributes=True)
+ for owner in getattr(chart, "owners", [])
+ ]
+ if getattr(chart, "owners", None)
+ else [],
+ )
+
+
+class ChartFilter(ColumnOperator):
+ """
+ Filter object for chart listing.
+ col: The column to filter on. Must be one of the allowed filter fields.
+ opr: The operator to use. Must be one of the supported operators.
+ value: The value to filter by (type depends on col and opr).
+ """
+
+ col: Literal[
+ "slice_name",
+ "viz_type",
+ "datasource_name",
+ ] = Field(
+ ...,
+ description="Column to filter on. See get_chart_available_filters for "
+ "allowed values.",
+ )
+ opr: ColumnOperatorEnum = Field(
+ ...,
+ description="Operator to use. See get_chart_available_filters for "
+ "allowed values.",
+ )
+ value: str | int | float | bool | List[str | int | float | bool] = Field(
+ ..., description="Value to filter by (type depends on col and opr)"
+ )
+
+
+class ChartList(BaseModel):
+ charts: List[ChartInfo]
+ count: int
+ total_count: int
+ page: int
+ page_size: int
+ total_pages: int
+ has_previous: bool
+ has_next: bool
+ columns_requested: List[str] | None = None
+ columns_loaded: List[str] | None = None
+ filters_applied: List[ChartFilter] = Field(
+ default_factory=list,
+ description="List of advanced filter dicts applied to the query.",
+ )
+ pagination: PaginationInfo | None = None
+ timestamp: datetime | None = None
+ model_config = ConfigDict(ser_json_timedelta="iso8601")
+
+
+# --- Simplified schemas for generate_chart tool ---
+
+
+# Common pieces
+class ColumnRef(BaseModel):
+ name: str = Field(
+ ...,
+ description="Column name",
+ min_length=1,
+ max_length=255,
+ pattern=r"^[a-zA-Z0-9_][a-zA-Z0-9_\s\-\.]*$",
+ )
+ label: str | None = Field(
+ None, description="Display label for the column", max_length=500
+ )
+ dtype: str | None = Field(None, description="Data type hint")
+ aggregate: (
+ Literal[
+ "SUM",
+ "COUNT",
+ "AVG",
+ "MIN",
+ "MAX",
+ "COUNT_DISTINCT",
+ "STDDEV",
+ "VAR",
+ "MEDIAN",
+ "PERCENTILE",
+ ]
+ | None
+ ) = Field(
+ None,
+ description="SQL aggregation function. Only these validated functions are "
+ "supported to prevent SQL errors.",
+ )
+
+ @field_validator("name")
+ @classmethod
+ def sanitize_name(cls, v: str) -> str:
+ """Sanitize column name to prevent XSS and SQL injection."""
+ if not v or not v.strip():
+ raise ValueError("Column name cannot be empty")
+
+ # Remove HTML tags and decode entities
+ sanitized = html.escape(v.strip())
+
+ # Check for script content
+ if re.search(r"<script[^>]*>.*?</script>", v, re.IGNORECASE | re.DOTALL):
+ raise ValueError(
+ "Column name contains potentially malicious script content"
+ )
+
+ # Basic SQL injection patterns (basic protection)
+ dangerous_patterns = [
+ r"(;|\||&|\$|`)",
+ r"\b(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER|EXEC|EXECUTE)\b",
+ r"--",
+ r"/\*.*\*/",
+ ]
+
+ for pattern in dangerous_patterns:
+ if re.search(pattern, v, re.IGNORECASE):
+ raise ValueError(
+ "Column name contains potentially unsafe characters or SQL keywords"
+ )
+
+ return sanitized
+
+ @field_validator("label")
+ @classmethod
+ def sanitize_label(cls, v: str | None) -> str | None:
+ """Sanitize display label to prevent XSS attacks."""
+ if v is None:
+ return v
+
+ # Strip whitespace
+ v = v.strip()
+ if not v:
+ return None
+
+ # Check for dangerous HTML tags and JavaScript protocols BEFORE escaping
+ dangerous_patterns = [
+ r"<script[^>]*>.*?</script>", # Script tags
+ r"<iframe[^>]*>.*?</iframe>", # Iframe tags
+ r"<object[^>]*>.*?</object>", # Object tags
+ r"<embed[^>]*>.*?</embed>", # Embed tags
+ r"<link[^>]*>", # Link tags
+ r"<meta[^>]*>", # Meta tags
+ r"javascript:", # JavaScript protocol
+ r"vbscript:", # VBScript protocol
+ r"data:text/html", # Data URL HTML
+ r"on\w+\s*=", # Event handlers (onclick, onload, etc)
+ ]
+
+ for pattern in dangerous_patterns:
+ if re.search(pattern, v, re.IGNORECASE | re.DOTALL):
+ raise ValueError(
+ "Label contains potentially malicious content. "
+ "HTML tags, JavaScript, and event handlers are not allowed in "
+ "labels."
+ )
+
+ # Filter dangerous Unicode characters
+ v = re.sub(
+ r"[\u200B-\u200D\uFEFF\u0000-\u0008\u000B\u000C\u000E-\u001F]", "", v
+ )
+
+ # HTML escape the cleaned content
+ sanitized = html.escape(v)
+
+ return sanitized if sanitized else None
+
+
+class AxisConfig(BaseModel):
+ title: str | None = Field(None, description="Axis title", max_length=200)
+ scale: Literal["linear", "log"] | None = Field(
+ "linear", description="Axis scale type"
+ )
+ format: str | None = Field(
+ None, description="Format string (e.g. '$,.2f')", max_length=50
+ )
+
+
+class LegendConfig(BaseModel):
+ show: bool = Field(True, description="Whether to show legend")
+ position: Literal["top", "bottom", "left", "right"] | None = Field(
+ "right", description="Legend position"
+ )
+
+
+class FilterConfig(BaseModel):
+ column: str = Field(
+ ..., description="Column to filter on", min_length=1, max_length=255
+ )
+ op: Literal["=", ">", "<", ">=", "<=", "!="] = Field(
+ ..., description="Filter operator"
+ )
+ value: str | int | float | bool = Field(..., description="Filter value")
+
+ @field_validator("column")
+ @classmethod
+ def sanitize_column(cls, v: str) -> str:
+ """Sanitize filter column name to prevent injection attacks."""
+ if not v or not v.strip():
+ raise ValueError("Filter column name cannot be empty")
+
+ # Remove HTML tags and decode entities
+ sanitized = html.escape(v.strip())
+
+ # Check for dangerous patterns
+ if re.search(r"<script[^>]*>.*?</script>", v, re.IGNORECASE | re.DOTALL):
+ raise ValueError(
+ "Filter column contains potentially malicious script content"
+ )
+
+ return sanitized
+
+ @field_validator("value")
+ @classmethod
+ def sanitize_value(cls, v: str | int | float | bool) -> str | int | float | bool:
+ """Sanitize filter value to prevent XSS and SQL injection attacks."""
+ if isinstance(v, str):
+ # Strip whitespace
+ v = v.strip()
+
+ # Check for dangerous patterns (SQL injection, XSS, script injection)
+ dangerous_patterns = [
+ # SQL injection patterns
+ r";\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER|EXEC|EXECUTE)\b",
+ r"'\s*OR\s*'",
+ r"'\s*AND\s*'",
+ r"--\s*",
+ r"/\*.*?\*/",
+ r"UNION\s+SELECT",
+ r"xp_cmdshell",
+ r"sp_executesql",
+ # XSS patterns
+ r"<script[^>]*>.*?</script>",
+ r"<iframe[^>]*>.*?</iframe>",
+ r"<object[^>]*>.*?</object>",
+ r"<embed[^>]*>.*?</embed>",
+ r"javascript:",
+ r"vbscript:",
+ r"data:text/html",
+ r"on\w+\s*=",
+ # Command injection patterns
+ r"[;&|`$()]",
+ r"\\x[0-9a-fA-F]{2}", # Hex encoding
+ ]
+
+ for pattern in dangerous_patterns:
+ if re.search(pattern, v, re.IGNORECASE | re.DOTALL):
+ raise ValueError(
+ "Filter value contains potentially malicious content. "
+ "SQL injection attempts, HTML tags, JavaScript, and command "
+ "injection "
+ "are not allowed in filter values."
+ )
+
+ # Additional length check for filter values
+ if len(v) > 1000:
Review Comment:
## Bad HTML filtering regexp
This regular expression does not match script end tags like </script >.
[Show more details](https://github.com/apache/superset/security/code-scanning/2045)
##########
superset/mcp_service/screenshot/webdriver_config.py:
##########
@@ -0,0 +1,139 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""
+WebDriver pool configuration defaults for Superset MCP service
+"""
+
+from typing import Any, Dict
+
+# Default WebDriver pool configuration
+DEFAULT_WEBDRIVER_POOL_CONFIG = {
+ # Maximum number of WebDriver instances to keep in the pool
+ "MAX_POOL_SIZE": 5,
+ # Maximum age of a WebDriver instance (in seconds)
+ # After this time, the driver will be destroyed and recreated
+ "MAX_AGE_SECONDS": 3600, # 1 hour
+ # Maximum number of times a WebDriver can be reused
+ # After this many uses, the driver will be destroyed and recreated
+ "MAX_USAGE_COUNT": 50,
+ # How long a WebDriver can sit idle before being destroyed (in seconds)
+ "IDLE_TIMEOUT_SECONDS": 300, # 5 minutes
+ # How often to perform health checks on WebDriver instances (in seconds)
+ "HEALTH_CHECK_INTERVAL": 60, # 1 minute
+}
+
+
+def configure_webdriver_pool(app_config: Dict[str, Any]) -> None:
+ """
+ Configure WebDriver pool settings in Superset app config.
+
+ This function adds WebDriver pool configuration to the Superset app config
+ if it doesn't already exist, using sensible defaults.
+
+ Args:
+ app_config: The Superset application configuration dictionary
+ """
+ if "WEBDRIVER_POOL" not in app_config:
+ app_config["WEBDRIVER_POOL"] = DEFAULT_WEBDRIVER_POOL_CONFIG.copy()
+ else:
+ # Merge with defaults for any missing keys
+ for key, default_value in DEFAULT_WEBDRIVER_POOL_CONFIG.items():
+ if key not in app_config["WEBDRIVER_POOL"]:
+ app_config["WEBDRIVER_POOL"][key] = default_value
+
+
+def get_pool_stats_endpoint() -> Any:
+ """
+ Create a Flask endpoint to view WebDriver pool statistics.
+
+ This function can be called to register a debugging endpoint
+ that shows the current state of the WebDriver pool.
+
+ Returns:
+ Flask route function for pool statistics
+ """
+
+ def pool_stats() -> Any:
+ try:
+ from flask import jsonify
+
+ from superset.mcp_service.screenshot.webdriver_pool import (
+ get_webdriver_pool,
+ )
+
+ pool = get_webdriver_pool()
+ stats = pool.get_stats()
+
+ return jsonify({"webdriver_pool": stats, "status": "healthy"})
+ except Exception as e:
+ from flask import jsonify
+
+ return jsonify({"error": str(e), "status": "error"}), 500
Review Comment:
## Information exposure through an exception
[Stack trace information](1) flows to this location and may be exposed to an external user.
[Show more details](https://github.com/apache/superset/security/code-scanning/2065)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(mcp): PR6 - add instance info tool, prompts, and resources [superset]
Posted by "codecov[bot] (via GitHub)" <gi...@apache.org>.
codecov[bot] commented on PR #35839:
URL: https://github.com/apache/superset/pull/35839#issuecomment-3445365725
## [Codecov](https://app.codecov.io/gh/apache/superset/pull/35839?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
:x: Patch coverage is `0.06443%` with `1551 lines` in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 44.70%. Comparing base ([`76d897e`](https://app.codecov.io/gh/apache/superset/commit/76d897eaa2f9e137102bc194c2e3109c29d0348f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`2deb19a`](https://app.codecov.io/gh/apache/superset/commit/2deb19a0dfa9a5a35e1feaf74f726d7b8d317fb7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
:warning: Report is 2790 commits behind head on master.
| [Files with missing lines](https://app.codecov.io/gh/apache/superset/pull/35839?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Patch % | Lines |
|---|---|---|
| [superset/mcp\_service/middleware.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fmiddleware.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvbWlkZGxld2FyZS5weQ==) | 0.00% | [275 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/screenshot/webdriver\_pool.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fscreenshot%2Fwebdriver_pool.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2Uvc2NyZWVuc2hvdC93ZWJkcml2ZXJfcG9vbC5weQ==) | 0.00% | [218 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/mcp\_core.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fmcp_core.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvbWNwX2NvcmUucHk=) | 0.00% | [193 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [...perset/mcp\_service/screenshot/pooled\_screenshot.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fscreenshot%2Fpooled_screenshot.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2Uvc2NyZWVuc2hvdC9wb29sZWRfc2NyZWVuc2hvdC5weQ==) | 0.00% | [172 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/utils/retry\_utils.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Futils%2Fretry_utils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvdXRpbHMvcmV0cnlfdXRpbHMucHk=) | 0.00% | [112 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/utils/error\_builder.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Futils%2Ferror_builder.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvdXRpbHMvZXJyb3JfYnVpbGRlci5weQ==) | 0.00% | [93 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/sql\_lab/sql\_lab\_utils.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fsql_lab%2Fsql_lab_utils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2Uvc3FsX2xhYi9zcWxfbGFiX3V0aWxzLnB5) | 0.00% | [86 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/sql\_lab/execute\_sql\_core.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fsql_lab%2Fexecute_sql_core.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2Uvc3FsX2xhYi9leGVjdXRlX3NxbF9jb3JlLnB5) | 0.00% | [58 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/utils/url\_utils.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Futils%2Furl_utils.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2UvdXRpbHMvdXJsX3V0aWxzLnB5) | 0.00% | [42 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| [superset/mcp\_service/sql\_lab/schemas.py](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&filepath=superset%2Fmcp_service%2Fsql_lab%2Fschemas.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#diff-c3VwZXJzZXQvbWNwX3NlcnZpY2Uvc3FsX2xhYi9zY2hlbWFzLnB5) | 0.00% | [41 Missing :warning: ](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
| ... and [15 more](https://app.codecov.io/gh/apache/superset/pull/35839?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | |
<details><summary>Additional details and impacted files</summary>
```diff
@@ Coverage Diff @@
## master #35839 +/- ##
===========================================
- Coverage 60.48% 44.70% -15.78%
===========================================
Files 1931 624 -1307
Lines 76236 45843 -30393
Branches 8568 4937 -3631
===========================================
- Hits 46114 20496 -25618
+ Misses 28017 25007 -3010
+ Partials 2105 340 -1765
```
| [Flag](https://app.codecov.io/gh/apache/superset/pull/35839/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | |
|---|---|---|
| [hive](https://app.codecov.io/gh/apache/superset/pull/35839/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `44.37% <0.06%> (-4.79%)` | :arrow_down: |
| [javascript](https://app.codecov.io/gh/apache/superset/pull/35839/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
| [presto](https://app.codecov.io/gh/apache/superset/pull/35839/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
| [python](https://app.codecov.io/gh/apache/superset/pull/35839/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `44.37% <0.06%> (-19.14%)` | :arrow_down: |
| [superset-extensions-cli](https://app.codecov.io/gh/apache/superset/pull/35839/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `94.80% <ø> (?)` | |
| [unit](https://app.codecov.io/gh/apache/superset/pull/35839/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `?` | |
Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache#carryforward-flags-in-the-pull-request-comment) to find out more.
</details>
[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/apache/superset/pull/35839?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
<details><summary> :rocket: New features to boost your workflow: </summary>
- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(mcp): PR6 - add instance info tool, prompts, and resources [superset]
Posted by "bito-code-review[bot] (via GitHub)" <gi...@apache.org>.
bito-code-review[bot] commented on PR #35839:
URL: https://github.com/apache/superset/pull/35839#issuecomment-3445305905
AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org
Re: [PR] feat(mcp): PR6 - add instance info tool, prompts, and resources [superset]
Posted by "aminghadersohi (via GitHub)" <gi...@apache.org>.
aminghadersohi closed pull request #35839: feat(mcp): PR6 - add instance info tool, prompts, and resources
URL: https://github.com/apache/superset/pull/35839
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org