Two files, one repository
apache/superset ships 3 formats across 3 indexed files. The question worth asking is whether the second one says anything the first does not.
| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 46 | 60 | 0% |
| Commands | 0 | 16 | 0 | 0% |
| Section tags | 7 | 6 | 4 | 41% |
What each file covers
Sections
0 shared · 46 only in A · 60 only in B- − LLM Context Guide for Apache Superset
- − Run Pre-commit Before Pushing
- − Stage your changes first
- − Run pre-commit on staged files
- − If there are auto-fixes, stage them and commit
- − ⚠️ CRITICAL: Ongoing Refactors (What NOT to Do)
- − Frontend Modernization
- − Testing Strategy Migration
- − Backend Type Safety
- − UUID Migration
- − Security and Threat Model
- − Key Directories
- − Code Standards
- − TypeScript Frontend
- − Python Backend
- − Apache License Headers
- − Code Comments
- − Documentation Requirements
- − Developer Portal: Storybook-to-MDX Documentation
- − Core Philosophy
- − Story Requirements for Docs Generation
- − Generator Location
- − Architecture Patterns
- − Security & Features
- − Test Utilities
- − Python Test Helpers
- − TypeScript Test Helpers
- − Test Database Patterns
- − Running Tests
- − Frontend
- − E2E Tests (Playwright - NEW)
- − E2E Tests (Cypress - DEPRECATED)
- − Backend
- − If pytest fails with database/setup issues, ask the user to run test environment setup
- − Environment Validation
- − Verify Superset is running
- − SQLAlchemy Query Best Practices
- − Pull Request Guidelines
- − Pre-commit Validation
- − Install hooks
- − IMPORTANT: Stage your changes first!
- − Quick validation (faster than --all-files)
- − Common File Patterns
- − API Structure
- − Migration Files
- − Platform-Specific Instructions
- + MCP Service - LLM Agent Guide
- + CRITICAL: Apache License Headers
- + 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.
- + Architecture Overview
- + Key Components
- + Dependency Injection Architecture
- + Critical Convention: Tool, Prompt, and Resource Registration
- + How to Add a New Tool
- + superset/mcp_service/chart/tool/my_new_tool.py
- + superset/mcp_service/app.py (at the bottom, after initialize_core_mcp_dependencies())
- + How to Add a New Prompt
- + superset/mcp_service/chart/prompts/my_new_prompt.py
- + How to Add a New Resource
- + superset/mcp_service/chart/resources/my_new_resource.py
- + Tool Development Patterns
- + 1. Tool Decorator Parameters
- + 2. Use Core Classes for Reusability
- + 3. Authentication and RBAC
- + Authentication + RBAC enabled (default)
- + Public tool (no auth) - use sparingly, and add the tool name to
- + ALLOWED_UNPROTECTED in app.py (e.g. generate_bug_report)
- + 4. Use Pydantic Schemas
- + 5. Follow the DAO Pattern
- + GOOD: Use DAO
- + BAD: Don't query directly
- + 6. Python Type Hints (Python 3.10+ Style)
- + GOOD - Modern Python 3.10+ syntax
- + BAD - Old-style (DO NOT USE)
- + 7. Event Logger Instrumentation
- + 8. Context Logging
- + 9. Error Handling
- + 10. Dataset Validation for Chart Tools
- + 11. Compile Check for Chart Creation
- + 12. Flexible Input Parsing
- + Middleware
- + Configuration
- + Authentication
- + RBAC
- + Embedded guest auth (opt-in; requires the EMBEDDED_SUPERSET feature flag).
- + Reuses core GUEST_TOKEN_JWT_* config — no MCP-specific guest secret/audience.
- + Default-deny: the ONLY tools a guest may call (everything else is denied).
- + Principal-agnostic extension point: given the current user, return an allow-list
- + (only these tools are callable) or None if unrestricted. Defaults to restricting
- + embedded guests to MCP_GUEST_ALLOWED_TOOLS; override to add other restricted
- + principals without touching the enforcement path.
- + Response Caching (optional, uses in-memory store by default; Redis when MCP_STORE_CONFIG enabled)
- + Multi-pod Storage (optional, requires Redis)
Commands
0 shared · 16 only in A · 0 only in B- − git add .
- − git commit --amend
- − npm run test
- − npm run test -- filename.test.tsx
- − npm run playwright:test
- − npm run playwright:ui
- − npm run playwright:headed
- − npx playwright test tests/auth/login.spec.ts
- − npm run playwright:debug tests/auth/login.spec.ts
- − npm run cypress-run-chrome
- − npm run cypress-debug
- − pytest
- − pytest tests/unit_tests/specific_test.py
- − pytest tests/unit_tests/
- − npm run dev
- − npm run lint
Section tags
7 shared · 6 only in A · 4 only in B- − setup
- − lint-format
- − git-pr
- − database
- − api
- − docs
- + build
- + performance
- + deployment
- + do-not
- test
- code-style
- architecture
- types
- testing-strategy
- security
- agent-behaviour
Line diff
apache/superset · AGENTS.md
@@ −1 @@
1# LLM Context Guide for Apache Superset
2
3Apache Superset is a data visualization platform with Flask/Python backend and React/TypeScript frontend.
4
5## Run Pre-commit Before Pushing
6
7Always run pre-commit against the files changed by the current branch before
8pushing. This matches CI and keeps unrelated failures already present on
9`master` from blocking otherwise independent work.
10
11```bash
12# Stage your changes first
13git add .
14
15# Run pre-commit on staged files
16pre-commit run
17
18# If there are auto-fixes, stage them and commit
19git add .
20git commit --amend # or new commit
21```
22
23Use `pre-commit run --all-files` when auditing or repairing the repository-wide
24baseline. If that check finds failures in files untouched by the current branch,
25fix them in a separate branch rather than adding unrelated changes to the
26current pull request.
27
28Common pre-commit failures:
29- **Formatting** - black, oxfmt, eslint will auto-fix
30- **Type errors** - mypy failures need manual fixes
31- **Linting** - ruff, pylint issues need manual fixes
32
33## ⚠️ CRITICAL: Ongoing Refactors (What NOT to Do)
34
35**These migrations are actively happening - avoid deprecated patterns:**
36
37### Frontend Modernization
38- **NO `any` types** - Use proper TypeScript types
39- **NO JavaScript files** - Convert to TypeScript (.ts/.tsx)
40- **Use @superset-ui/core** - Don't import Ant Design directly, prefer Ant Design component wrappers from @superset-ui/core/components
41- **Use antd theming tokens** - Prefer antd tokens over legacy theming tokens
42- **Avoid custom css and styles** - Follow antd best practices and avoid styling and custom CSS whenever possible
43
44### Testing Strategy Migration
45- **Prefer unit tests** over integration tests
46- **Prefer integration tests** over end-to-end tests
47- **Use Playwright for E2E tests** - Migrating from Cypress
48- **Cypress is deprecated** - Will be removed once migration is completed
49- **Use Jest + React Testing Library** for component testing
50- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
51
52### Backend Type Safety
53- **Add type hints** - All new Python code needs proper typing
54- **MyPy compliance** - Run `pre-commit run mypy` to validate
55- **SQLAlchemy typing** - Use proper model annotations
56
57### UUID Migration
58- **Prefer UUIDs over auto-incrementing IDs** - New models should use UUID primary keys
59- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
60- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
61
62## Security and Threat Model
63
64Before evaluating any code path for security issues, read [`SECURITY.md`](SECURITY.md). It is the canonical, authoritative source for Apache Superset's security model and is referenced by both human reporters and automated scanners.
65
66In short, the test for whether a finding is in scope is one question:
67
68> *Does it let a principal perform an action the role and capability matrix in `SECURITY.md` does not entitle them to?*
69
70If yes, it is in scope. If no, it is not.
71
72The three trust boundaries are:
73
741. **The Admin role** is a fully trusted operational principal. Anything an Admin can do through documented configuration, API, or UI is an intended capability, not a vulnerability.
752. **The operator** owns deployment-time decisions (secrets, network exposure, feature-flag selection, connector and codec choices, notification destinations, third-party plugins). Misconfiguration at this layer is a deployment defect, not a Superset vulnerability.
763. **The codebase** is responsible for enforcing the role and capability matrix wherever it exposes functionality to a principal: API routes, command and DAO layers, UI handlers, background jobs, and any other entry point. A missing or incorrect enforcement check is in scope no matter where it lives.
77
78The security model assumes that operator-controlled infrastructure, including the metadata database, cache backends, message brokers, secret stores, and deployment environment, remains within the operator's trust boundary. Vulnerabilities must demonstrate a security boundary violation by an attacker who does not already control those systems.
79
80Route-level authorization in this codebase uses one of three Flask-AppBuilder decorators depending on the route type:
81
82- `@protect()` for REST API routes (`ModelRestApi` / `BaseApi`)
83- `@has_access_api` for legacy view routes
84- `@has_access` for legacy HTML view routes
85
86Object-level authorization via `security_manager.raise_for_access(...)` applies to data-bearing resources: dashboards, charts, datasets and datasources, queries, database and table access, and query contexts. Other resources (annotations, tags, CSS templates, reports, RLS rules, and similar) rely on the route-level decorator plus DAO `base_filters` for ownership scoping; the absence of `raise_for_access` on these resources is by design, not a finding. Code that omits the per-object gate on a route that returns or mutates a specific data-bearing object is in scope; code that follows the correct pattern for its resource class can still contain injection, SSRF, XSS, or other classes of finding unrelated to authorization, which are evaluated separately.
87
88The full role and capability matrix, in-scope and out-of-scope class lists, and CVE aggregation rules are in [`SECURITY.md`](SECURITY.md). Defer to that document for any specifics.
89
90**Requirements for findings filed by automated tooling**
91
92Automated scanners (LLM-based code scanners, static analyzers, dependency tools) that file findings against this codebase must, in each finding, name:
93
941. The specific role and capability matrix row in [`SECURITY.md`](SECURITY.md) the finding believes is violated.
952. The principal the finding assumes the attacker holds (Public, Gamma, sql_lab, Alpha, Admin, Embedded guest token, or a custom role with explicit capability grants).
96
97Findings that cannot identify both should be filed as questions, not vulnerabilities. This requirement exists to ensure every reported issue is testable against the published security model and to keep speculative or pattern-match-only reports out of the triage queue.
98
99## Key Directories
100
101```
102superset/
103├── superset/ # Python backend (Flask, SQLAlchemy)
104│ ├── views/api/ # REST API endpoints
105│ ├── models/ # Database models
106│ └── connectors/ # Database connections
107├── superset-frontend/src/ # React TypeScript frontend
108│ ├── components/ # Reusable components
109│ ├── explore/ # Chart builder
110│ ├── dashboard/ # Dashboard interface
111│ └── SqlLab/ # SQL editor
112├── superset-frontend/packages/
113│ └── superset-ui-core/ # UI component library (USE THIS)
114├── tests/ # Python/integration tests
115├── docs/ # Documentation (UPDATE FOR CHANGES)
116└── UPDATING.md # Breaking changes log
117```
118
119## Code Standards
120
121### TypeScript Frontend
122- **Avoid `any` types** - Use proper TypeScript, reuse existing types
123- **Functional components** with hooks
124- **@superset-ui/core** for UI components (not direct antd)
125- **Jest** for testing (NO Enzyme)
126- **Redux** for global state where it exists, hooks for local
127
128### Python Backend
129- **Type hints required** for all new code
130- **MyPy compliant** - run `pre-commit run mypy`
131- **SQLAlchemy models** with proper typing
132- **pytest** for testing
133
134### Apache License Headers
135- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
136- **LLM instruction files are excluded** - Files like AGENTS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
137
138### Code Comments
139- **Avoid time-specific language** - Don't use words like "now", "currently", "today" in code comments as they become outdated
140- **Write timeless comments** - Comments should remain accurate regardless of when they're read
141
142## Documentation Requirements
143
144- **docs/**: Update for any user-facing changes
145- **UPDATING.md**: Add breaking changes here
146- **Docstrings**: Required for new functions/classes
147
148## Developer Portal: Storybook-to-MDX Documentation
149
150The Developer Portal auto-generates MDX documentation from Storybook stories. **Stories are the single source of truth.**
151
152### Core Philosophy
153- **Fix issues in the STORY, not the generator** - When something doesn't render correctly, update the story file first
154- **Generator should be lightweight** - It extracts and passes through data; avoid special cases
155- **Stories define everything** - Props, controls, galleries, examples all come from story metadata
156
157### Story Requirements for Docs Generation
158- Use `export default { title: '...' }` (inline), not `const meta = ...; export default meta;`
159- Name interactive stories `Interactive${ComponentName}` (e.g., `InteractiveButton`)
160- Define `args` for default prop values
161- Define `argTypes` at the story level (not meta level) with control types and descriptions
162- Use `parameters.docs.gallery` for size×style variant grids
163- Use `parameters.docs.sampleChildren` for components that need children
164- Use `parameters.docs.liveExample` for custom live code blocks
165- Use `parameters.docs.staticProps` for complex object props that can't be parsed inline
166
167### Generator Location
168- Script: `docs/scripts/generate-superset-components.mjs`
169- Wrapper: `docs/src/components/StorybookWrapper.jsx`
170- Output: `docs/developer_portal/components/`
171
172## Architecture Patterns
173
174### Security & Features
175- **Security model**: see the top-level [Security and Threat Model](#security-and-threat-model) section and [`SECURITY.md`](SECURITY.md)
176- **RBAC**: Role-based access via Flask-AppBuilder
177- **Feature flags**: Control feature rollouts
178- **Row-level security**: SQL-based data access control
179
180## Test Utilities
181
182### Python Test Helpers
183- **`SupersetTestCase`** - Base class in `tests/integration_tests/base_tests.py`
184- **`@with_config`** - Config mocking decorator
185- **`@with_feature_flags`** - Feature flag testing
186- **`login_as()`, `login_as_admin()`** - Authentication helpers
187- **`create_dashboard()`, `create_slice()`** - Data setup utilities
188
189### TypeScript Test Helpers
190- **`superset-frontend/spec/helpers/testing-library.tsx`** - Custom render() with providers
191- **`createWrapper()`** - Redux/Router/Theme wrapper
192- **`selectOption()`** - Select component helper
193- **React Testing Library** - NO Enzyme (removed)
194
195### Test Database Patterns
196- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
197- **API tests**: Update expected columns when adding new model fields
198
199### Running Tests
200```bash
201# Frontend
202npm run test # All tests
203npm run test -- filename.test.tsx # Single file
204
205# E2E Tests (Playwright - NEW)
206npm run playwright:test # All Playwright tests
207npm run playwright:ui # Interactive UI mode
208npm run playwright:headed # See browser during tests
209npx playwright test tests/auth/login.spec.ts # Single file
210npm run playwright:debug tests/auth/login.spec.ts # Debug specific file
211
212# E2E Tests (Cypress - DEPRECATED)
213cd superset-frontend/cypress-base
214npm run cypress-run-chrome # All Cypress tests (headless)
215npm run cypress-debug # Interactive Cypress UI
216
217# Backend
218pytest # All tests
219pytest tests/unit_tests/specific_test.py # Single file
220pytest tests/unit_tests/ # Directory
221
222# If pytest fails with database/setup issues, ask the user to run test environment setup
223```
224
225## Environment Validation
226
227**Quick Setup Check (run this first):**
228
229```bash
230# Verify Superset is running
231curl -f http://localhost:8088/health || echo "❌ Setup required - see https://superset.apache.org/docs/contributing/development#working-with-llms"
232```
233
234**If health checks fail:**
235"It appears you aren't set up properly. Please refer to the [Working with LLMs](https://superset.apache.org/docs/contributing/development#working-with-llms) section in the development docs for setup instructions."
236
237**Key Project Files:**
238- `superset-frontend/package.json` - Frontend build scripts (`npm run dev` on port 9000, `npm run test`, `npm run lint`)
239- `pyproject.toml` - Python tooling (ruff, mypy configs)
240- `requirements/` folder - Python dependencies (base.txt, development.txt)
241
242## SQLAlchemy Query Best Practices
243- **Use negation operator**: `~Model.field` instead of `== False` to avoid ruff E712 errors
244- **Example**: `~Model.is_active` instead of `Model.is_active == False`
245
246## Pull Request Guidelines
247
248**When creating pull requests:**
249
2501. **Read the current PR template**: Always check `.github/PULL_REQUEST_TEMPLATE.md` for the latest format
2512. **Use the template sections**: Include all sections from the template (SUMMARY, BEFORE/AFTER, TESTING INSTRUCTIONS, ADDITIONAL INFORMATION)
2523. **Follow PR title conventions**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
253 - Format: `type(scope): description`
254 - Example: `fix(dashboard): load charts correctly`
255 - Types: `fix`, `feat`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`
256
257**Important**: Always reference the actual template file at `.github/PULL_REQUEST_TEMPLATE.md` instead of using cached content, as the template may be updated over time.
258
259## Pre-commit Validation
260
261**Use pre-commit hooks for quality validation:**
262
263```bash
264# Install hooks
265pre-commit install
266
267# IMPORTANT: Stage your changes first!
268git add . # Pre-commit only checks staged files
269
270# Quick validation (faster than --all-files)
271pre-commit run # Staged files only
272pre-commit run mypy # Python type checking
273pre-commit run format # Code formatting
274pre-commit run eslint # Frontend linting
275```
276
277**Important pre-commit usage notes:**
278- **Stage files first**: Run `git add .` before `pre-commit run` to check only changed files (much faster)
279- **Virtual environment**: Activate your Python virtual environment before running pre-commit
280 ```bash
281 # Common virtual environment locations (yours may differ):
282 source .venv/bin/activate # if using .venv
283 source venv/bin/activate # if using venv
284 source ~/venvs/superset/bin/activate # if using a central location
285 ```
286 If you get a "command not found" error, ask the user which virtual environment to activate
287- **Auto-fixes**: Some hooks auto-fix issues (e.g., trailing whitespace). Re-run after fixes are applied
288
289## Common File Patterns
290
291### API Structure
292- **`/api.py`** - REST endpoints with decorators and OpenAPI docstrings
293- **`/schemas.py`** - Marshmallow validation schemas for OpenAPI spec
294- **`/commands/`** - Business logic classes with @transaction() decorators
295- **`/models/`** - SQLAlchemy database models
296- **OpenAPI docs**: Auto-generated at `/swagger/v1` from docstrings and schemas
297
298### Migration Files
299- **Location**: `superset/migrations/versions/`
300- **Naming**: `YYYY-MM-DD_HH-MM_hash_description.py`
301- **Utilities**: Use helpers from `superset.migrations.shared.utils` for database compatibility
302- **Pattern**: Import utilities instead of raw SQLAlchemy operations
303
304## Platform-Specific Instructions
305
306- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
307- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
308- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
309- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
310- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor
311
312---
313
314**LLM Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.
315
apache/superset · superset/mcp_service/CLAUDE.md
@@ +1 @@
1# MCP Service - LLM Agent Guide
2
3This guide helps LLM agents understand the Superset MCP (Model Context Protocol) service architecture and development conventions.
4
5## CRITICAL: Apache License Headers
6
7**EVERY Python file in the MCP service MUST have the Apache Software Foundation license header.**
8
9This includes:
10- All `.py` files (tool files, schemas, __init__.py files, etc.)
11- **NEVER remove existing license headers during refactoring or edits**
12- **ALWAYS add license headers when creating new files**
13- **ALWAYS verify license headers are present after editing files**
14
15If you see a file without a license header, ADD IT IMMEDIATELY. If you accidentally remove one during editing, ADD IT BACK.
16
17Use this exact template at the top of EVERY Python file:
18
19```python
20# Licensed to the Apache Software Foundation (ASF) under one
21# or more contributor license agreements. See the NOTICE file
22# distributed with this work for additional information
23# regarding copyright ownership. The ASF licenses this file
24# to you under the Apache License, Version 2.0 (the
25# "License"); you may not use this file except in compliance
26# with the License. You may obtain a copy of the License at
27#
28# http://www.apache.org/licenses/LICENSE-2.0
29#
30# Unless required by applicable law or agreed to in writing,
31# software distributed under the License is distributed on an
32# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
33# KIND, either express or implied. See the License for the
34# specific language governing permissions and limitations
35# under the License.
36```
37
38**Note**: LLM instruction files like `CLAUDE.md`, `AGENTS.md`, etc. are excluded from this requirement (listed in `.rat-excludes`) to avoid token overhead, but ALL other Python files require it.
39
40## Architecture Overview
41
42The MCP service provides programmatic access to Superset via the Model Context Protocol, allowing AI assistants to interact with dashboards, charts, datasets, databases, SQL Lab, and instance metadata.
43
44### Key Components
45
46```
47superset/mcp_service/
48├── app.py # FastMCP app factory and tool registration
49├── auth.py # Authentication, authorization, and RBAC
50├── mcp_config.py # Default configuration
51├── mcp_core.py # Reusable core classes for tools
52├── flask_singleton.py # Flask app singleton for MCP context
53├── middleware.py # FastMCP middleware (logging, errors, size guards)
54├── server.py # Server startup (streamable-http, multi-pod)
55├── jwt_verifier.py # JWT token validation
56├── chart/ # Chart tools, schemas, prompts, resources
57│ ├── schemas.py
58│ ├── chart_utils.py
59│ ├── preview_utils.py
60│ ├── validation.py
61│ ├── tool/
62│ ├── prompts/
63│ └── resources/
64├── dashboard/ # Dashboard tools and schemas
65│ ├── schemas.py
66│ └── tool/
67├── dataset/ # Dataset tools and schemas
68│ ├── schemas.py
69│ └── tool/
70├── explore/ # Explore link generation
71│ ├── schemas.py
72│ └── tool/
73├── sql_lab/ # SQL Lab tools (execute, save, open)
74│ ├── schemas.py
75│ └── tool/
76├── system/ # System tools (health, instance info, schema)
77│ ├── schemas.py
78│ ├── tool/
79│ ├── prompts/
80│ └── resources/
81├── common/ # Shared error schemas
82├── commands/ # MCP-specific command classes
83└── utils/ # Utilities (URL, schema parsing, error builders)
84```
85
86### Dependency Injection Architecture
87
88The `@tool` and `@prompt` decorators are defined as stubs in the `superset-core` package (`superset_core.mcp.decorators`). At startup, `app.py` calls `initialize_core_mcp_dependencies()` which replaces these stubs with concrete implementations that register tools/prompts with the FastMCP instance. This avoids circular imports between `superset_core` and `superset`.
89
90**Startup flow**:
911. `app.py` creates the FastMCP `mcp` instance
922. `initialize_core_mcp_dependencies()` injects the real decorator implementations
933. Tool/prompt/resource imports at the bottom of `app.py` trigger registration
944. `server.py` adds middleware and starts the transport
95
96## Critical Convention: Tool, Prompt, and Resource Registration
97
98**IMPORTANT**: When creating new MCP tools, prompts, or resources, you MUST add their imports to `app.py` for auto-registration. Do NOT add them to `server.py` - that approach doesn't work properly.
99
100### How to Add a New Tool
101
1021. **Create the tool file** in the appropriate directory (e.g., `chart/tool/my_new_tool.py`)
1032. **Decorate with `@tool`** using the decorator from `superset_core.mcp.decorators`
1043. **Export from the module's `__init__.py`** (e.g., `chart/tool/__init__.py`)
1054. **Add import to `app.py`** at the bottom of the file where other tools are imported
106
107**Example (read-only tool)**:
108```python
109# superset/mcp_service/chart/tool/my_new_tool.py
110from fastmcp import Context
111from superset_core.mcp.decorators import tool, ToolAnnotations
112
113from superset.extensions import event_logger
114
115@tool(
116 tags=["core"],
117 class_permission_name="Chart",
118 annotations=ToolAnnotations(
119 title="My new tool",
120 readOnlyHint=True,
121 destructiveHint=False,
122 ),
123)
124async def my_new_tool(request: MyRequest, ctx: Context) -> MyResponse:
125 """Tool description for LLMs."""
126 await ctx.info("Doing something: param=%s" % (request.param,))
127 with event_logger.log_context(action="mcp.my_new_tool"):
128 result = do_something()
129 return MyResponse(data=result)
130```
131
132**Example (mutating tool)**:
133```python
134@tool(
135 tags=["mutate"],
136 class_permission_name="Chart",
137 method_permission_name="write",
138 annotations=ToolAnnotations(
139 title="Create something",
140 readOnlyHint=False,
141 destructiveHint=False,
142 ),
143)
144async def create_something(request: CreateRequest, ctx: Context) -> CreateResponse:
145 """Creates a new resource."""
146 ...
147```
148
149**Then add to app.py**:
150```python
151# superset/mcp_service/app.py (at the bottom, after initialize_core_mcp_dependencies())
152from superset.mcp_service.chart.tool import ( # noqa: F401, E402
153 get_chart_info,
154 list_charts,
155 my_new_tool, # ADD YOUR TOOL HERE
156)
157```
158
159**Why this matters**: Tools register automatically on import via the `@tool` decorator. The import MUST be in `app.py` at the bottom (after `initialize_core_mcp_dependencies()` is called). DO NOT add imports to `server.py`.
160
161### How to Add a New Prompt
162
1631. **Create the prompt file** in the appropriate directory (e.g., `chart/prompts/my_new_prompt.py`)
1642. **Decorate with `@prompt`** from `superset_core.mcp.decorators`
1653. **Add import to module's `__init__.py`** (e.g., `chart/prompts/__init__.py`)
1664. **Ensure module is imported in `app.py`**
167
168**Example**:
169```python
170# superset/mcp_service/chart/prompts/my_new_prompt.py
171from superset_core.mcp.decorators import prompt
172
173@prompt("my_new_prompt")
174async def my_new_prompt_handler(
175 chart_type: str = "auto", business_goal: str = "exploration"
176) -> str:
177 """Interactive prompt for doing something."""
178 return "Prompt instructions here..."
179```
180
181### How to Add a New Resource
182
183Resources use direct FastMCP decorators and **must include `@mcp_auth_hook`** for authentication:
184
185```python
186# superset/mcp_service/chart/resources/my_new_resource.py
187from superset.mcp_service.app import mcp
188from superset.mcp_service.auth import mcp_auth_hook # REQUIRED for resources
189
190@mcp.resource("superset://chart/my_resource")
191@mcp_auth_hook # Always add this decorator to resources
192def get_my_resource() -> str:
193 """Resource description for LLMs."""
194 return "Resource data here..."
195```
196
197## Tool Development Patterns
198
199### 1. Tool Decorator Parameters
200
201The `@tool` decorator from `superset_core.mcp.decorators` accepts:
202
203- **`tags`**: List of tags (e.g., `["core"]`, `["mutate"]`). Default: `[]`
204- **`class_permission_name`**: FAB permission class (e.g., `"Chart"`, `"Dashboard"`). Default: `None`
205- **`method_permission_name`**: Permission action (e.g., `"read"`, `"write"`). Default: Auto — `"write"` if `"mutate"` in tags, else `"read"`
206- **`protect`**: Enable authentication wrapping. Default: `True`
207- **`annotations`**: MCP `ToolAnnotations` object. Default: `None`
208
209**ToolAnnotations** (from `superset_core.mcp.decorators`):
210```python
211annotations=ToolAnnotations(
212 title="Human-readable title",
213 readOnlyHint=True, # Whether tool only reads data
214 destructiveHint=False, # Whether tool has destructive side effects
215)
216```
217
218### 2. Use Core Classes for Reusability
219
220The `mcp_core.py` module provides reusable patterns:
221
222- **`ModelListCore`**: For listing resources with filtering, search, and pagination
223 - Used by: `list_charts`, `list_dashboards`, `list_datasets`, `list_databases`
224- **`ModelGetInfoCore`**: For getting resource details by ID, UUID, or slug
225 - Used by: `get_chart_info`, `get_dashboard_info`, `get_dataset_info`, `get_database_info`
226- **`ModelGetSchemaCore`**: For schema discovery (columns, filters, sortable columns)
227 - Used by: `get_schema`
228- **`InstanceInfoCore`**: For instance statistics and metadata
229 - Used by: `get_instance_info`
230
231### 3. Authentication and RBAC
232
233Authentication is handled automatically by the `@tool` decorator (via `mcp_auth_hook` internally). RBAC permission checking uses `class_permission_name` and `method_permission_name`.
234
235```python
236from superset_core.mcp.decorators import tool, ToolAnnotations
237
238# Authentication + RBAC enabled (default)
239@tool(
240 class_permission_name="Chart", # Checks user has Chart access
241)
242async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
243 # g.user is set automatically before this runs
244 ...
245
246# Public tool (no auth) - use sparingly, and add the tool name to
247# ALLOWED_UNPROTECTED in app.py (e.g. generate_bug_report)
248@tool(protect=False)
249async def public_status(ctx: Context) -> dict:
250 return {"status": "healthy"}
251```
252
253Note: `health_check` is a protected, authenticated tool (`@tool(tags=["core"], ...)`,
254no `protect=False`) — it is not an example of a public tool.
255
256**Authentication priority order** (in `auth.py`):
2571. JWT context (per-request ContextVar from FastMCP). Also resolves a verified
258 embedded **guest token** to a `GuestUser` when `MCP_EMBEDDED_GUEST_AUTH_ENABLED`
259 + `EMBEDDED_SUPERSET` are on (a guest is never downgraded to a lower priority).
2602. API Key authentication (via FAB SecurityManager)
2613. `MCP_DEV_USERNAME` config (development only)
2624. `g.user` fallback (set by external middleware)
263
264Guest tokens are verified by `GuestTokenVerifier` (in the `CompositeTokenVerifier`,
265before the JWT verifier) using the shared core `GUEST_TOKEN_JWT_*` config, then
266built into a `GuestUser` in `_resolve_user_from_jwt_context`. See `SECURITY.md`.
267
268**`@mcp_auth_hook`** is only used directly on **resources** — tools get auth wrapping from `@tool(protect=True)`.
269
270### 4. Use Pydantic Schemas
271
272**All tool inputs and outputs must be Pydantic models**. Place schemas in `{module}/schemas.py`.
273
274```python
275from pydantic import BaseModel, ConfigDict, Field
276
277class MyToolRequest(BaseModel):
278 model_config = ConfigDict(populate_by_name=True)
279
280 param: str = Field(..., description="Parameter description for LLMs")
281 optional_param: str | None = Field(None, description="Optional parameter")
282
283class MyToolResponse(BaseModel):
284 result: str = Field(..., description="Result description")
285 error: str | None = Field(None, description="Error message if failed")
286```
287
288### 5. Follow the DAO Pattern
289
290**Use Superset's DAO (Data Access Object) layer** instead of direct database queries:
291
292```python
293from superset.daos.dashboard import DashboardDAO
294
295# GOOD: Use DAO
296dashboard = DashboardDAO.find_by_id(dashboard_id)
297
298# BAD: Don't query directly
299dashboard = db.session.query(Dashboard).filter_by(id=dashboard_id).first()
300```
301
302### 6. Python Type Hints (Python 3.10+ Style)
303
304**CRITICAL**: Always use modern Python 3.10+ union syntax for type hints.
305
306```python
307# GOOD - Modern Python 3.10+ syntax
308from typing import Any
309
310from pydantic import BaseModel, Field
311
312class MySchema(BaseModel):
313 name: str | None = Field(None, description="Optional name")
314 tags: list[str] = Field(default_factory=list)
315 metadata: dict[str, Any] = Field(default_factory=dict)
316
317def my_function(
318 id: int,
319 filters: list[str] | None = None,
320) -> MySchema | None:
321 pass
322
323# BAD - Old-style (DO NOT USE)
324from typing import Optional, List, Dict
325name: Optional[str] # Wrong! Use str | None
326tags: List[str] # Wrong! Use list[str]
327```
328
329### 7. Event Logger Instrumentation
330
331**All tool operations should use `event_logger`** for observability:
332
333```python
334from superset.extensions import event_logger
335
336@tool(...)
337async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
338 with event_logger.log_context(action="mcp.my_tool.step_name"):
339 result = do_something()
340 return MyResponse(data=result)
341```
342
343### 8. Context Logging
344
345Use the FastMCP `Context` object for structured logging within tools:
346
347```python
348async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
349 await ctx.info("Starting: param=%s" % (request.param,))
350 await ctx.debug("Details: keys=%s" % (sorted(request.model_dump().keys()),))
351 await ctx.warning("Something unexpected: %s" % (warning_msg,))
352 await ctx.error("Failed: %s" % (str(exc),))
353 await ctx.report_progress(1, 5, "Step 1 of 5")
354```
355
356### 9. Error Handling
357
358**Pattern**: Catch specific exceptions for known failure modes, use broad `Exception` only as the outermost safety net that re-raises:
359
360```python
361from superset.commands.dataset.exceptions import DatasetInvalidError, DatasetCreateFailedError
362
363@tool(...)
364async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
365 try:
366 # Specific exception handling for known failure modes
367 with event_logger.log_context(action="mcp.my_tool"):
368 result = SomeCommand(properties).run()
369 return MyResponse(data=result)
370
371 except DatasetInvalidError as exc:
372 # Return structured error response (don't raise)
373 await ctx.error("Validation failed: %s" % (exc.normalized_messages(),))
374 return MyResponse(error=str(exc.normalized_messages()))
375
376 except DatasetCreateFailedError as exc:
377 await ctx.error("Creation failed: %s" % (str(exc),))
378 return MyResponse(error=f"Failed: {exc}")
379
380 except Exception as exc:
381 # Outermost safety net: log and re-raise (middleware handles it)
382 await ctx.error("Unexpected: %s: %s" % (type(exc).__name__, str(exc)))
383 raise
384```
385
386### 10. Dataset Validation for Chart Tools
387
388All chart-related tools must validate that the chart's dataset is accessible:
389
390```python
391from superset.mcp_service.chart.chart_utils import validate_chart_dataset
392
393validation_result = validate_chart_dataset(chart, check_access=True)
394if not validation_result.is_valid:
395 await ctx.warning("Dataset not accessible: %s" % (validation_result.error,))
396 return ChartError(
397 error=validation_result.error or "Chart's dataset is not accessible",
398 error_type="DatasetNotAccessible",
399 )
400```
401
402Used by: `get_chart_info`, `get_chart_preview`, `get_chart_data`, `generate_chart`
403
404### 11. Compile Check for Chart Creation
405
406When creating, saving, or previewing charts, run schema validation (Tier 1)
407and optionally a compile check (Tier 2) before persisting or caching.
408``validate_and_compile`` glues both together; tools with tight SLAs
409(``generate_explore_link``, ``update_chart_preview``) opt out of Tier 2.
410
411```python
412from superset.mcp_service.chart.compile import validate_and_compile
413
414result = validate_and_compile(
415 config, form_data, dataset, run_compile_check=True
416)
417if not result.success:
418 # ``result.error_obj`` is a ``ChartGenerationError`` with fuzzy-match
419 # suggestions ("did you mean sum_boys?") so the LLM can self-correct.
420 ...
421```
422
423The lower-level ``_compile_chart(form_data, dataset_id)`` is still exported
424for callers that have already done their own schema validation.
425
426### 12. Flexible Input Parsing
427
428`ModelListCore` handles JSON string vs. native object parsing automatically via utilities in `superset.mcp_service.utils.schema_utils`:
429
430- `parse_json_or_passthrough(value, param_name)` - JSON string or dict
431- `parse_json_or_list(value, param_name)` - JSON array, list, or comma-separated string
432- `parse_json_or_model(value, model_class, param_name)` - JSON string or dict to Pydantic model
433- `parse_json_or_model_list(value, model_class, param_name)` - JSON array to list of Pydantic models
434
435These are used internally by `ModelListCore` for `filters` and `select_columns`. Individual tools using core classes do NOT need to add parsing logic.
436
437## Middleware
438
439The MCP service uses FastMCP middleware (registered in `server.py`):
440
441- **`LoggingMiddleware`**: Logs tool calls with duration, entity IDs, sanitizes sensitive data
442- **`GlobalErrorHandlerMiddleware`**: Catches unhandled exceptions, converts to ToolError
443- **`StructuredContentStripperMiddleware`**: Strips structuredContent from responses (Claude.ai compatibility)
444- **`ResponseSizeGuardMiddleware`**: Prevents oversized responses from crashing clients
445- **`ResponseCachingMiddleware`**: Optional response caching (in-memory by default, Redis when store enabled)
446
447Middleware is applied in `server.py` and should NOT be modified in individual tools.
448
449## Configuration
450
451Default configuration is in `mcp_config.py`. Override in `superset_config.py`:
452
453```python
454# Authentication
455MCP_DEV_USERNAME = None # Fallback username for dev mode
456MCP_AUTH_ENABLED = False # Enable JWT/API key auth
457MCP_AUTH_FACTORY = None # Custom auth factory function
458MCP_JWT_PUBLIC_KEY = None
459MCP_JWT_SECRET = None
460MCP_JWKS_URI = None
461MCP_USER_RESOLVER = None # Custom function to extract username from JWT
462
463# RBAC
464MCP_RBAC_ENABLED = True # Enable permission checking (default: True)
465
466# Embedded guest auth (opt-in; requires the EMBEDDED_SUPERSET feature flag).
467# Reuses core GUEST_TOKEN_JWT_* config — no MCP-specific guest secret/audience.
468MCP_EMBEDDED_GUEST_AUTH_ENABLED = False
469# Default-deny: the ONLY tools a guest may call (everything else is denied).
470MCP_GUEST_ALLOWED_TOOLS = {
471 "get_dashboard_info", "get_dashboard_layout", "list_dashboards",
472 "list_charts", "get_chart_info", "get_chart_data", "get_chart_preview",
473}
474# Principal-agnostic extension point: given the current user, return an allow-list
475# (only these tools are callable) or None if unrestricted. Defaults to restricting
476# embedded guests to MCP_GUEST_ALLOWED_TOOLS; override to add other restricted
477# principals without touching the enforcement path.
478MCP_RESTRICTED_TOOL_POLICY = None # Callable[[user], frozenset[str] | None]
479
480
481# Response Caching (optional, uses in-memory store by default; Redis when MCP_STORE_CONFIG enabled)
482MCP_CACHE_CONFIG = {
483 "enabled": False,
484 "list_tools_ttl": 300,
485 "call_tool_ttl": 3600,
486 "excluded_tools": ["execute_sql", "generate_dashboard"], # add tools to exclude
487}
488
489# Multi-pod Storage (optional, requires Redis)
490MCP_STORE_CONFIG = {
491 "enabled": False,
492 "CACHE_REDIS_URL": None,
493 "event_store_ttl": 3600,
494}
495```
496
497## Testing Conventions
498
499### Test Organization
500
501Tests mirror the MCP service module structure:
502```
503tests/unit_tests/mcp_service/
504├── conftest.py # Global fixtures (disable_mcp_rbac)
505├── chart/
506│ ├── test_chart_utils.py
507│ ├── test_chart_schemas.py
508│ └── tool/
509│ ├── test_list_charts.py
510│ ├── test_generate_chart.py
511│ └── ...
512├── dashboard/tool/
513├── dataset/tool/
514├── sql_lab/tool/
515├── system/tool/
516├── test_auth_*.py # Auth/RBAC tests
517└── test_middleware*.py # Middleware tests
518```
519
520### Async Tool Tests (primary pattern)
521
522```python
523from unittest.mock import MagicMock, patch
524import pytest
525from fastmcp import Client
526
527from superset.mcp_service.app import mcp
528from superset.utils import json
529
530@pytest.fixture
531def mcp_server():
532 return mcp
533
534@pytest.mark.asyncio
535async def test_my_tool_success(mcp_server):
536 mock_obj = MagicMock()
537 mock_obj.id = 1
538 mock_obj.name = "test"
539
540 with patch("superset.daos.chart.ChartDAO.find_by_id", return_value=mock_obj):
541 async with Client(mcp_server) as client:
542 result = await client.call_tool(
543 "my_tool", {"request": {"id": 1}}
544 )
545 data = json.loads(result.content[0].text)
546
547 assert data["id"] == 1
548```
549
550### Key Testing Patterns
551
552- **RBAC is disabled globally** via `conftest.py` autouse fixture (`MCP_RBAC_ENABLED = False`)
553- **RBAC tests** are separate in `test_auth_rbac.py` with their own `enable_mcp_rbac` fixture
554- **Auth is mocked** via `mock_auth` fixture that patches `get_user_from_request`
555- **Mock objects** must have all attributes set explicitly (no auto-generation)
556- **Patch at the DAO level**: `patch("superset.daos.chart.ChartDAO.find_by_id", ...)`
557- **Schema validation tests** are synchronous (no Client needed)
558
559## Common Pitfalls to Avoid
560
561### 1. Forgetting Tool Import in app.py
562**Problem**: Tool exists but isn't available to MCP clients.
563**Solution**: Add tool import to `app.py` at the bottom (after `initialize_core_mcp_dependencies()`).
564
565### 2. Adding Tool Imports to server.py
566**Problem**: Tools won't register properly.
567**Solution**: Tool imports MUST be in `app.py`, not `server.py`.
568
569### 3. Wrong Decorator Import Path
570**Problem**: Using stale import path.
571**Solution**: Use `from superset_core.mcp.decorators import tool, ToolAnnotations` (NOT `superset_core.api.mcp`).
572
573### 4. Missing ToolAnnotations
574**Problem**: Tool lacks MCP directory compliance metadata.
575**Solution**: Always include `annotations=ToolAnnotations(title=..., readOnlyHint=..., destructiveHint=...)`.
576
577### 5. Using `Optional` Instead of Union Syntax
578**Problem**: Old-style `Optional[T]` is not Python 3.10+ style.
579**Solution**: Use `T | None` and `list[str]` instead of `Optional[T]` and `List[str]`.
580
581### 6. Direct Database Queries
582**Problem**: Bypasses Superset's security and caching layers.
583**Solution**: Use DAO classes (ChartDAO, DashboardDAO, DatasetDAO, DatabaseDAO).
584
585### 7. Not Using Core Classes
586**Problem**: Duplicating list/get_info logic across tools.
587**Solution**: Use `ModelListCore`, `ModelGetInfoCore`, `ModelGetSchemaCore`.
588
589### 8. Missing Apache License Headers
590**Problem**: CI fails on license check.
591**Solution**: Add ASF license header to all new `.py` files (see template at top of this doc).
592
593### 9. Circular Imports
594**Problem**: Importing from `app.py` in tool files causes circular dependencies.
595**Solution**: Use `from superset_core.mcp.decorators import tool` for tools/prompts. Only import `from superset.mcp_service.app import mcp` in resource files.
596
597### 10. Missing event_logger Instrumentation
598**Problem**: Tool operations are invisible to observability.
599**Solution**: Wrap key operations with `event_logger.log_context(action="mcp.tool_name.step")`.
600
601## Quick Checklist for New Tools
602
603- [ ] Created tool file in `{module}/tool/{tool_name}.py`
604- [ ] Added ASF license header
605- [ ] Used `@tool(tags=[...], class_permission_name="...", annotations=ToolAnnotations(...))` decorator
606- [ ] Import: `from superset_core.mcp.decorators import tool, ToolAnnotations`
607- [ ] Created Pydantic request/response schemas in `{module}/schemas.py`
608- [ ] Used DAO classes instead of direct queries
609- [ ] Added `event_logger.log_context()` instrumentation
610- [ ] Used `await ctx.info/error/debug()` for context logging
611- [ ] Exported from `{module}/tool/__init__.py`
612- [ ] Added tool import to `app.py` at the bottom
613- [ ] Created async unit tests in `tests/unit_tests/mcp_service/{module}/tool/`
614- [ ] Updated `DEFAULT_INSTRUCTIONS` in `app.py` if adding new capability
615
616## Quick Checklist for New Prompts
617
618- [ ] Created prompt file in `{module}/prompts/{prompt_name}.py`
619- [ ] Added ASF license header
620- [ ] Used `@prompt("prompt_name")` from `superset_core.mcp.decorators`
621- [ ] Made function async: `async def prompt_handler(...) -> str`
622- [ ] Added import to `{module}/prompts/__init__.py`
623- [ ] Verified module import exists in `app.py`
624
625## Quick Checklist for New Resources
626
627- [ ] Created resource file in `{module}/resources/{resource_name}.py`
628- [ ] Added ASF license header
629- [ ] Used `@mcp.resource("superset://{path}")` decorator
630- [ ] Added `@mcp_auth_hook` decorator
631- [ ] Added import to `{module}/resources/__init__.py`
632- [ ] Verified module import exists in `app.py`
633
634## Getting Help
635
636- Check existing tool implementations for patterns (chart/tool/, dashboard/tool/)
637- Review core classes in `mcp_core.py` for reusable functionality
638- See `CLAUDE.md` in project root for general Superset development guidelines
639- Consult Superset documentation: https://superset.apache.org/docs/
640
@@ −1 +1 @@
1−# LLM Context Guide for Apache Superset
1+# MCP Service - LLM Agent Guide
22
3−Apache Superset is a data visualization platform with Flask/Python backend and React/TypeScript frontend.
3+This guide helps LLM agents understand the Superset MCP (Model Context Protocol) service architecture and development conventions.
44
5−## Run Pre-commit Before Pushing
5+## CRITICAL: Apache License Headers
66
7−Always run pre-commit against the files changed by the current branch before
8−pushing. This matches CI and keeps unrelated failures already present on
9−`master` from blocking otherwise independent work.
7+**EVERY Python file in the MCP service MUST have the Apache Software Foundation license header.**
108
11−```bash
12−# Stage your changes first
13−git add .
9+This includes:
10+- All `.py` files (tool files, schemas, __init__.py files, etc.)
11+- **NEVER remove existing license headers during refactoring or edits**
12+- **ALWAYS add license headers when creating new files**
13+- **ALWAYS verify license headers are present after editing files**
1414
15−# Run pre-commit on staged files
16−pre-commit run
15+If you see a file without a license header, ADD IT IMMEDIATELY. If you accidentally remove one during editing, ADD IT BACK.
1716
18−# If there are auto-fixes, stage them and commit
19−git add .
20−git commit --amend # or new commit
17+Use this exact template at the top of EVERY Python file:
18+
19+```python
20+# Licensed to the Apache Software Foundation (ASF) under one
21+# or more contributor license agreements. See the NOTICE file
22+# distributed with this work for additional information
23+# regarding copyright ownership. The ASF licenses this file
24+# to you under the Apache License, Version 2.0 (the
25+# "License"); you may not use this file except in compliance
26+# with the License. You may obtain a copy of the License at
27+#
28+# http://www.apache.org/licenses/LICENSE-2.0
29+#
30+# Unless required by applicable law or agreed to in writing,
31+# software distributed under the License is distributed on an
32+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
33+# KIND, either express or implied. See the License for the
34+# specific language governing permissions and limitations
35+# under the License.
2136 ```
2237
23−Use `pre-commit run --all-files` when auditing or repairing the repository-wide
24−baseline. If that check finds failures in files untouched by the current branch,
25−fix them in a separate branch rather than adding unrelated changes to the
26−current pull request.
38+**Note**: LLM instruction files like `CLAUDE.md`, `AGENTS.md`, etc. are excluded from this requirement (listed in `.rat-excludes`) to avoid token overhead, but ALL other Python files require it.
2739
28−Common pre-commit failures:
29−- **Formatting** - black, oxfmt, eslint will auto-fix
30−- **Type errors** - mypy failures need manual fixes
31−- **Linting** - ruff, pylint issues need manual fixes
40+## Architecture Overview
3241
33−## ⚠️ CRITICAL: Ongoing Refactors (What NOT to Do)
42+The MCP service provides programmatic access to Superset via the Model Context Protocol, allowing AI assistants to interact with dashboards, charts, datasets, databases, SQL Lab, and instance metadata.
3443
35−**These migrations are actively happening - avoid deprecated patterns:**
44+### Key Components
3645
37−### Frontend Modernization
38−- **NO `any` types** - Use proper TypeScript types
39−- **NO JavaScript files** - Convert to TypeScript (.ts/.tsx)
40−- **Use @superset-ui/core** - Don't import Ant Design directly, prefer Ant Design component wrappers from @superset-ui/core/components
41−- **Use antd theming tokens** - Prefer antd tokens over legacy theming tokens
42−- **Avoid custom css and styles** - Follow antd best practices and avoid styling and custom CSS whenever possible
46+```
47+superset/mcp_service/
48+├── app.py # FastMCP app factory and tool registration
49+├── auth.py # Authentication, authorization, and RBAC
50+├── mcp_config.py # Default configuration
51+├── mcp_core.py # Reusable core classes for tools
52+├── flask_singleton.py # Flask app singleton for MCP context
53+├── middleware.py # FastMCP middleware (logging, errors, size guards)
54+├── server.py # Server startup (streamable-http, multi-pod)
55+├── jwt_verifier.py # JWT token validation
56+├── chart/ # Chart tools, schemas, prompts, resources
57+│ ├── schemas.py
58+│ ├── chart_utils.py
59+│ ├── preview_utils.py
60+│ ├── validation.py
61+│ ├── tool/
62+│ ├── prompts/
63+│ └── resources/
64+├── dashboard/ # Dashboard tools and schemas
65+│ ├── schemas.py
66+│ └── tool/
67+├── dataset/ # Dataset tools and schemas
68+│ ├── schemas.py
69+│ └── tool/
70+├── explore/ # Explore link generation
71+│ ├── schemas.py
72+│ └── tool/
73+├── sql_lab/ # SQL Lab tools (execute, save, open)
74+│ ├── schemas.py
75+│ └── tool/
76+├── system/ # System tools (health, instance info, schema)
77+│ ├── schemas.py
78+│ ├── tool/
79+│ ├── prompts/
80+│ └── resources/
81+├── common/ # Shared error schemas
82+├── commands/ # MCP-specific command classes
83+└── utils/ # Utilities (URL, schema parsing, error builders)
84+```
4385
44−### Testing Strategy Migration
45−- **Prefer unit tests** over integration tests
46−- **Prefer integration tests** over end-to-end tests
47−- **Use Playwright for E2E tests** - Migrating from Cypress
48−- **Cypress is deprecated** - Will be removed once migration is completed
49−- **Use Jest + React Testing Library** for component testing
50−- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
86+### Dependency Injection Architecture
5187
52−### Backend Type Safety
53−- **Add type hints** - All new Python code needs proper typing
54−- **MyPy compliance** - Run `pre-commit run mypy` to validate
55−- **SQLAlchemy typing** - Use proper model annotations
88+The `@tool` and `@prompt` decorators are defined as stubs in the `superset-core` package (`superset_core.mcp.decorators`). At startup, `app.py` calls `initialize_core_mcp_dependencies()` which replaces these stubs with concrete implementations that register tools/prompts with the FastMCP instance. This avoids circular imports between `superset_core` and `superset`.
5689
57−### UUID Migration
58−- **Prefer UUIDs over auto-incrementing IDs** - New models should use UUID primary keys
59−- **External API exposure** - Use UUIDs in public APIs instead of internal integer IDs
60−- **Existing models** - Add UUID fields alongside integer IDs for gradual migration
90+**Startup flow**:
91+1. `app.py` creates the FastMCP `mcp` instance
92+2. `initialize_core_mcp_dependencies()` injects the real decorator implementations
93+3. Tool/prompt/resource imports at the bottom of `app.py` trigger registration
94+4. `server.py` adds middleware and starts the transport
6195
62−## Security and Threat Model
96+## Critical Convention: Tool, Prompt, and Resource Registration
6397
64−Before evaluating any code path for security issues, read [`SECURITY.md`](SECURITY.md). It is the canonical, authoritative source for Apache Superset's security model and is referenced by both human reporters and automated scanners.
98+**IMPORTANT**: When creating new MCP tools, prompts, or resources, you MUST add their imports to `app.py` for auto-registration. Do NOT add them to `server.py` - that approach doesn't work properly.
6599
66−In short, the test for whether a finding is in scope is one question:
100+### How to Add a New Tool
67101
68−> *Does it let a principal perform an action the role and capability matrix in `SECURITY.md` does not entitle them to?*
102+1. **Create the tool file** in the appropriate directory (e.g., `chart/tool/my_new_tool.py`)
103+2. **Decorate with `@tool`** using the decorator from `superset_core.mcp.decorators`
104+3. **Export from the module's `__init__.py`** (e.g., `chart/tool/__init__.py`)
105+4. **Add import to `app.py`** at the bottom of the file where other tools are imported
69106
70−If yes, it is in scope. If no, it is not.
107+**Example (read-only tool)**:
108+```python
109+# superset/mcp_service/chart/tool/my_new_tool.py
110+from fastmcp import Context
111+from superset_core.mcp.decorators import tool, ToolAnnotations
71112
72−The three trust boundaries are:
113+from superset.extensions import event_logger
73114
74−1. **The Admin role** is a fully trusted operational principal. Anything an Admin can do through documented configuration, API, or UI is an intended capability, not a vulnerability.
75−2. **The operator** owns deployment-time decisions (secrets, network exposure, feature-flag selection, connector and codec choices, notification destinations, third-party plugins). Misconfiguration at this layer is a deployment defect, not a Superset vulnerability.
76−3. **The codebase** is responsible for enforcing the role and capability matrix wherever it exposes functionality to a principal: API routes, command and DAO layers, UI handlers, background jobs, and any other entry point. A missing or incorrect enforcement check is in scope no matter where it lives.
115+@tool(
116+ tags=["core"],
117+ class_permission_name="Chart",
118+ annotations=ToolAnnotations(
119+ title="My new tool",
120+ readOnlyHint=True,
121+ destructiveHint=False,
122+ ),
123+)
124+async def my_new_tool(request: MyRequest, ctx: Context) -> MyResponse:
125+ """Tool description for LLMs."""
126+ await ctx.info("Doing something: param=%s" % (request.param,))
127+ with event_logger.log_context(action="mcp.my_new_tool"):
128+ result = do_something()
129+ return MyResponse(data=result)
130+```
77131
78−The security model assumes that operator-controlled infrastructure, including the metadata database, cache backends, message brokers, secret stores, and deployment environment, remains within the operator's trust boundary. Vulnerabilities must demonstrate a security boundary violation by an attacker who does not already control those systems.
132+**Example (mutating tool)**:
133+```python
134+@tool(
135+ tags=["mutate"],
136+ class_permission_name="Chart",
137+ method_permission_name="write",
138+ annotations=ToolAnnotations(
139+ title="Create something",
140+ readOnlyHint=False,
141+ destructiveHint=False,
142+ ),
143+)
144+async def create_something(request: CreateRequest, ctx: Context) -> CreateResponse:
145+ """Creates a new resource."""
146+ ...
147+```
79148
80−Route-level authorization in this codebase uses one of three Flask-AppBuilder decorators depending on the route type:
149+**Then add to app.py**:
150+```python
151+# superset/mcp_service/app.py (at the bottom, after initialize_core_mcp_dependencies())
152+from superset.mcp_service.chart.tool import ( # noqa: F401, E402
153+ get_chart_info,
154+ list_charts,
155+ my_new_tool, # ADD YOUR TOOL HERE
156+)
157+```
81158
82−- `@protect()` for REST API routes (`ModelRestApi` / `BaseApi`)
83−- `@has_access_api` for legacy view routes
84−- `@has_access` for legacy HTML view routes
159+**Why this matters**: Tools register automatically on import via the `@tool` decorator. The import MUST be in `app.py` at the bottom (after `initialize_core_mcp_dependencies()` is called). DO NOT add imports to `server.py`.
85160
86−Object-level authorization via `security_manager.raise_for_access(...)` applies to data-bearing resources: dashboards, charts, datasets and datasources, queries, database and table access, and query contexts. Other resources (annotations, tags, CSS templates, reports, RLS rules, and similar) rely on the route-level decorator plus DAO `base_filters` for ownership scoping; the absence of `raise_for_access` on these resources is by design, not a finding. Code that omits the per-object gate on a route that returns or mutates a specific data-bearing object is in scope; code that follows the correct pattern for its resource class can still contain injection, SSRF, XSS, or other classes of finding unrelated to authorization, which are evaluated separately.
161+### How to Add a New Prompt
87162
88−The full role and capability matrix, in-scope and out-of-scope class lists, and CVE aggregation rules are in [`SECURITY.md`](SECURITY.md). Defer to that document for any specifics.
163+1. **Create the prompt file** in the appropriate directory (e.g., `chart/prompts/my_new_prompt.py`)
164+2. **Decorate with `@prompt`** from `superset_core.mcp.decorators`
165+3. **Add import to module's `__init__.py`** (e.g., `chart/prompts/__init__.py`)
166+4. **Ensure module is imported in `app.py`**
89167
90−**Requirements for findings filed by automated tooling**
168+**Example**:
169+```python
170+# superset/mcp_service/chart/prompts/my_new_prompt.py
171+from superset_core.mcp.decorators import prompt
91172
92−Automated scanners (LLM-based code scanners, static analyzers, dependency tools) that file findings against this codebase must, in each finding, name:
173+@prompt("my_new_prompt")
174+async def my_new_prompt_handler(
175+ chart_type: str = "auto", business_goal: str = "exploration"
176+) -> str:
177+ """Interactive prompt for doing something."""
178+ return "Prompt instructions here..."
179+```
93180
94−1. The specific role and capability matrix row in [`SECURITY.md`](SECURITY.md) the finding believes is violated.
95−2. The principal the finding assumes the attacker holds (Public, Gamma, sql_lab, Alpha, Admin, Embedded guest token, or a custom role with explicit capability grants).
181+### How to Add a New Resource
96182
97−Findings that cannot identify both should be filed as questions, not vulnerabilities. This requirement exists to ensure every reported issue is testable against the published security model and to keep speculative or pattern-match-only reports out of the triage queue.
183+Resources use direct FastMCP decorators and **must include `@mcp_auth_hook`** for authentication:
98184
99−## Key Directories
185+```python
186+# superset/mcp_service/chart/resources/my_new_resource.py
187+from superset.mcp_service.app import mcp
188+from superset.mcp_service.auth import mcp_auth_hook # REQUIRED for resources
100189
190+@mcp.resource("superset://chart/my_resource")
191+@mcp_auth_hook # Always add this decorator to resources
192+def get_my_resource() -> str:
193+ """Resource description for LLMs."""
194+ return "Resource data here..."
101195 ```
102−superset/
103−├── superset/ # Python backend (Flask, SQLAlchemy)
104−│ ├── views/api/ # REST API endpoints
105−│ ├── models/ # Database models
106−│ └── connectors/ # Database connections
107−├── superset-frontend/src/ # React TypeScript frontend
108−│ ├── components/ # Reusable components
109−│ ├── explore/ # Chart builder
110−│ ├── dashboard/ # Dashboard interface
111−│ └── SqlLab/ # SQL editor
112−├── superset-frontend/packages/
113−│ └── superset-ui-core/ # UI component library (USE THIS)
114−├── tests/ # Python/integration tests
115−├── docs/ # Documentation (UPDATE FOR CHANGES)
116−└── UPDATING.md # Breaking changes log
196+
197+## Tool Development Patterns
198+
199+### 1. Tool Decorator Parameters
200+
201+The `@tool` decorator from `superset_core.mcp.decorators` accepts:
202+
203+- **`tags`**: List of tags (e.g., `["core"]`, `["mutate"]`). Default: `[]`
204+- **`class_permission_name`**: FAB permission class (e.g., `"Chart"`, `"Dashboard"`). Default: `None`
205+- **`method_permission_name`**: Permission action (e.g., `"read"`, `"write"`). Default: Auto — `"write"` if `"mutate"` in tags, else `"read"`
206+- **`protect`**: Enable authentication wrapping. Default: `True`
207+- **`annotations`**: MCP `ToolAnnotations` object. Default: `None`
208+
209+**ToolAnnotations** (from `superset_core.mcp.decorators`):
210+```python
211+annotations=ToolAnnotations(
212+ title="Human-readable title",
213+ readOnlyHint=True, # Whether tool only reads data
214+ destructiveHint=False, # Whether tool has destructive side effects
215+)
117216 ```
118217
119−## Code Standards
218+### 2. Use Core Classes for Reusability
120219
121−### TypeScript Frontend
122−- **Avoid `any` types** - Use proper TypeScript, reuse existing types
123−- **Functional components** with hooks
124−- **@superset-ui/core** for UI components (not direct antd)
125−- **Jest** for testing (NO Enzyme)
126−- **Redux** for global state where it exists, hooks for local
220+The `mcp_core.py` module provides reusable patterns:
127221
128−### Python Backend
129−- **Type hints required** for all new code
130−- **MyPy compliant** - run `pre-commit run mypy`
131−- **SQLAlchemy models** with proper typing
132−- **pytest** for testing
222+- **`ModelListCore`**: For listing resources with filtering, search, and pagination
223+ - Used by: `list_charts`, `list_dashboards`, `list_datasets`, `list_databases`
224+- **`ModelGetInfoCore`**: For getting resource details by ID, UUID, or slug
225+ - Used by: `get_chart_info`, `get_dashboard_info`, `get_dataset_info`, `get_database_info`
226+- **`ModelGetSchemaCore`**: For schema discovery (columns, filters, sortable columns)
227+ - Used by: `get_schema`
228+- **`InstanceInfoCore`**: For instance statistics and metadata
229+ - Used by: `get_instance_info`
133230
134−### Apache License Headers
135−- **New files require ASF license headers** - When creating new code files, include the standard Apache Software Foundation license header
136−- **LLM instruction files are excluded** - Files like AGENTS.md, CLAUDE.md, etc. are in `.rat-excludes` to avoid header token overhead
231+### 3. Authentication and RBAC
137232
138−### Code Comments
139−- **Avoid time-specific language** - Don't use words like "now", "currently", "today" in code comments as they become outdated
140−- **Write timeless comments** - Comments should remain accurate regardless of when they're read
233+Authentication is handled automatically by the `@tool` decorator (via `mcp_auth_hook` internally). RBAC permission checking uses `class_permission_name` and `method_permission_name`.
141234
142−## Documentation Requirements
235+```python
236+from superset_core.mcp.decorators import tool, ToolAnnotations
143237
144−- **docs/**: Update for any user-facing changes
145−- **UPDATING.md**: Add breaking changes here
146−- **Docstrings**: Required for new functions/classes
238+# Authentication + RBAC enabled (default)
239+@tool(
240+ class_permission_name="Chart", # Checks user has Chart access
241+)
242+async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
243+ # g.user is set automatically before this runs
244+ ...
147245
148−## Developer Portal: Storybook-to-MDX Documentation
246+# Public tool (no auth) - use sparingly, and add the tool name to
247+# ALLOWED_UNPROTECTED in app.py (e.g. generate_bug_report)
248+@tool(protect=False)
249+async def public_status(ctx: Context) -> dict:
250+ return {"status": "healthy"}
251+```
149252
150−The Developer Portal auto-generates MDX documentation from Storybook stories. **Stories are the single source of truth.**
253+Note: `health_check` is a protected, authenticated tool (`@tool(tags=["core"], ...)`,
254+no `protect=False`) — it is not an example of a public tool.
151255
152−### Core Philosophy
153−- **Fix issues in the STORY, not the generator** - When something doesn't render correctly, update the story file first
154−- **Generator should be lightweight** - It extracts and passes through data; avoid special cases
155−- **Stories define everything** - Props, controls, galleries, examples all come from story metadata
256+**Authentication priority order** (in `auth.py`):
257+1. JWT context (per-request ContextVar from FastMCP). Also resolves a verified
258+ embedded **guest token** to a `GuestUser` when `MCP_EMBEDDED_GUEST_AUTH_ENABLED`
259+ + `EMBEDDED_SUPERSET` are on (a guest is never downgraded to a lower priority).
260+2. API Key authentication (via FAB SecurityManager)
261+3. `MCP_DEV_USERNAME` config (development only)
262+4. `g.user` fallback (set by external middleware)
156263
157−### Story Requirements for Docs Generation
158−- Use `export default { title: '...' }` (inline), not `const meta = ...; export default meta;`
159−- Name interactive stories `Interactive${ComponentName}` (e.g., `InteractiveButton`)
160−- Define `args` for default prop values
161−- Define `argTypes` at the story level (not meta level) with control types and descriptions
162−- Use `parameters.docs.gallery` for size×style variant grids
163−- Use `parameters.docs.sampleChildren` for components that need children
164−- Use `parameters.docs.liveExample` for custom live code blocks
165−- Use `parameters.docs.staticProps` for complex object props that can't be parsed inline
264+Guest tokens are verified by `GuestTokenVerifier` (in the `CompositeTokenVerifier`,
265+before the JWT verifier) using the shared core `GUEST_TOKEN_JWT_*` config, then
266+built into a `GuestUser` in `_resolve_user_from_jwt_context`. See `SECURITY.md`.
166267
167−### Generator Location
168−- Script: `docs/scripts/generate-superset-components.mjs`
169−- Wrapper: `docs/src/components/StorybookWrapper.jsx`
170−- Output: `docs/developer_portal/components/`
268+**`@mcp_auth_hook`** is only used directly on **resources** — tools get auth wrapping from `@tool(protect=True)`.
171269
172−## Architecture Patterns
270+### 4. Use Pydantic Schemas
173271
174−### Security & Features
175−- **Security model**: see the top-level [Security and Threat Model](#security-and-threat-model) section and [`SECURITY.md`](SECURITY.md)
176−- **RBAC**: Role-based access via Flask-AppBuilder
177−- **Feature flags**: Control feature rollouts
178−- **Row-level security**: SQL-based data access control
272+**All tool inputs and outputs must be Pydantic models**. Place schemas in `{module}/schemas.py`.
179273
180−## Test Utilities
274+```python
275+from pydantic import BaseModel, ConfigDict, Field
181276
182−### Python Test Helpers
183−- **`SupersetTestCase`** - Base class in `tests/integration_tests/base_tests.py`
184−- **`@with_config`** - Config mocking decorator
185−- **`@with_feature_flags`** - Feature flag testing
186−- **`login_as()`, `login_as_admin()`** - Authentication helpers
187−- **`create_dashboard()`, `create_slice()`** - Data setup utilities
277+class MyToolRequest(BaseModel):
278+ model_config = ConfigDict(populate_by_name=True)
188279
189−### TypeScript Test Helpers
190−- **`superset-frontend/spec/helpers/testing-library.tsx`** - Custom render() with providers
191−- **`createWrapper()`** - Redux/Router/Theme wrapper
192−- **`selectOption()`** - Select component helper
193−- **React Testing Library** - NO Enzyme (removed)
280+ param: str = Field(..., description="Parameter description for LLMs")
281+ optional_param: str | None = Field(None, description="Optional parameter")
194282
195−### Test Database Patterns
196−- **Mock patterns**: Use `MagicMock()` for config objects, avoid `AsyncMock` for synchronous code
197−- **API tests**: Update expected columns when adding new model fields
283+class MyToolResponse(BaseModel):
284+ result: str = Field(..., description="Result description")
285+ error: str | None = Field(None, description="Error message if failed")
286+```
198287
199−### Running Tests
200−```bash
201−# Frontend
202−npm run test # All tests
203−npm run test -- filename.test.tsx # Single file
288+### 5. Follow the DAO Pattern
204289
205−# E2E Tests (Playwright - NEW)
206−npm run playwright:test # All Playwright tests
207−npm run playwright:ui # Interactive UI mode
208−npm run playwright:headed # See browser during tests
209−npx playwright test tests/auth/login.spec.ts # Single file
210−npm run playwright:debug tests/auth/login.spec.ts # Debug specific file
290+**Use Superset's DAO (Data Access Object) layer** instead of direct database queries:
211291
212−# E2E Tests (Cypress - DEPRECATED)
213−cd superset-frontend/cypress-base
214−npm run cypress-run-chrome # All Cypress tests (headless)
215−npm run cypress-debug # Interactive Cypress UI
292+```python
293+from superset.daos.dashboard import DashboardDAO
216294
217−# Backend
218−pytest # All tests
219−pytest tests/unit_tests/specific_test.py # Single file
220−pytest tests/unit_tests/ # Directory
295+# GOOD: Use DAO
296+dashboard = DashboardDAO.find_by_id(dashboard_id)
221297
222−# If pytest fails with database/setup issues, ask the user to run test environment setup
298+# BAD: Don't query directly
299+dashboard = db.session.query(Dashboard).filter_by(id=dashboard_id).first()
223300 ```
224301
225−## Environment Validation
302+### 6. Python Type Hints (Python 3.10+ Style)
226303
227−**Quick Setup Check (run this first):**
304+**CRITICAL**: Always use modern Python 3.10+ union syntax for type hints.
228305
229−```bash
230−# Verify Superset is running
231−curl -f http://localhost:8088/health || echo "❌ Setup required - see https://superset.apache.org/docs/contributing/development#working-with-llms"
306+```python
307+# GOOD - Modern Python 3.10+ syntax
308+from typing import Any
309+
310+from pydantic import BaseModel, Field
311+
312+class MySchema(BaseModel):
313+ name: str | None = Field(None, description="Optional name")
314+ tags: list[str] = Field(default_factory=list)
315+ metadata: dict[str, Any] = Field(default_factory=dict)
316+
317+def my_function(
318+ id: int,
319+ filters: list[str] | None = None,
320+) -> MySchema | None:
321+ pass
322+
323+# BAD - Old-style (DO NOT USE)
324+from typing import Optional, List, Dict
325+name: Optional[str] # Wrong! Use str | None
326+tags: List[str] # Wrong! Use list[str]
232327 ```
233328
234−**If health checks fail:**
235−"It appears you aren't set up properly. Please refer to the [Working with LLMs](https://superset.apache.org/docs/contributing/development#working-with-llms) section in the development docs for setup instructions."
329+### 7. Event Logger Instrumentation
236330
237−**Key Project Files:**
238−- `superset-frontend/package.json` - Frontend build scripts (`npm run dev` on port 9000, `npm run test`, `npm run lint`)
239−- `pyproject.toml` - Python tooling (ruff, mypy configs)
240−- `requirements/` folder - Python dependencies (base.txt, development.txt)
331+**All tool operations should use `event_logger`** for observability:
241332
242−## SQLAlchemy Query Best Practices
243−- **Use negation operator**: `~Model.field` instead of `== False` to avoid ruff E712 errors
244−- **Example**: `~Model.is_active` instead of `Model.is_active == False`
333+```python
334+from superset.extensions import event_logger
245335
246−## Pull Request Guidelines
336+@tool(...)
337+async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
338+ with event_logger.log_context(action="mcp.my_tool.step_name"):
339+ result = do_something()
340+ return MyResponse(data=result)
341+```
247342
248−**When creating pull requests:**
343+### 8. Context Logging
249344
250−1. **Read the current PR template**: Always check `.github/PULL_REQUEST_TEMPLATE.md` for the latest format
251−2. **Use the template sections**: Include all sections from the template (SUMMARY, BEFORE/AFTER, TESTING INSTRUCTIONS, ADDITIONAL INFORMATION)
252−3. **Follow PR title conventions**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)
253− - Format: `type(scope): description`
254− - Example: `fix(dashboard): load charts correctly`
255− - Types: `fix`, `feat`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`
345+Use the FastMCP `Context` object for structured logging within tools:
256346
257−**Important**: Always reference the actual template file at `.github/PULL_REQUEST_TEMPLATE.md` instead of using cached content, as the template may be updated over time.
347+```python
348+async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
349+ await ctx.info("Starting: param=%s" % (request.param,))
350+ await ctx.debug("Details: keys=%s" % (sorted(request.model_dump().keys()),))
351+ await ctx.warning("Something unexpected: %s" % (warning_msg,))
352+ await ctx.error("Failed: %s" % (str(exc),))
353+ await ctx.report_progress(1, 5, "Step 1 of 5")
354+```
258355
259−## Pre-commit Validation
356+### 9. Error Handling
260357
261−**Use pre-commit hooks for quality validation:**
358+**Pattern**: Catch specific exceptions for known failure modes, use broad `Exception` only as the outermost safety net that re-raises:
262359
263−```bash
264−# Install hooks
265−pre-commit install
360+```python
361+from superset.commands.dataset.exceptions import DatasetInvalidError, DatasetCreateFailedError
266362
267−# IMPORTANT: Stage your changes first!
268−git add . # Pre-commit only checks staged files
363+@tool(...)
364+async def my_tool(request: MyRequest, ctx: Context) -> MyResponse:
365+ try:
366+ # Specific exception handling for known failure modes
367+ with event_logger.log_context(action="mcp.my_tool"):
368+ result = SomeCommand(properties).run()
369+ return MyResponse(data=result)
269370
270−# Quick validation (faster than --all-files)
271−pre-commit run # Staged files only
272−pre-commit run mypy # Python type checking
273−pre-commit run format # Code formatting
274−pre-commit run eslint # Frontend linting
371+ except DatasetInvalidError as exc:
372+ # Return structured error response (don't raise)
373+ await ctx.error("Validation failed: %s" % (exc.normalized_messages(),))
374+ return MyResponse(error=str(exc.normalized_messages()))
375+
376+ except DatasetCreateFailedError as exc:
377+ await ctx.error("Creation failed: %s" % (str(exc),))
378+ return MyResponse(error=f"Failed: {exc}")
379+
380+ except Exception as exc:
381+ # Outermost safety net: log and re-raise (middleware handles it)
382+ await ctx.error("Unexpected: %s: %s" % (type(exc).__name__, str(exc)))
383+ raise
275384 ```
276385
277−**Important pre-commit usage notes:**
278−- **Stage files first**: Run `git add .` before `pre-commit run` to check only changed files (much faster)
279−- **Virtual environment**: Activate your Python virtual environment before running pre-commit
280− ```bash
281− # Common virtual environment locations (yours may differ):
282− source .venv/bin/activate # if using .venv
283− source venv/bin/activate # if using venv
284− source ~/venvs/superset/bin/activate # if using a central location
285− ```
286− If you get a "command not found" error, ask the user which virtual environment to activate
287−- **Auto-fixes**: Some hooks auto-fix issues (e.g., trailing whitespace). Re-run after fixes are applied
386+### 10. Dataset Validation for Chart Tools
288387
289−## Common File Patterns
388+All chart-related tools must validate that the chart's dataset is accessible:
290389
291−### API Structure
292−- **`/api.py`** - REST endpoints with decorators and OpenAPI docstrings
293−- **`/schemas.py`** - Marshmallow validation schemas for OpenAPI spec
294−- **`/commands/`** - Business logic classes with @transaction() decorators
295−- **`/models/`** - SQLAlchemy database models
296−- **OpenAPI docs**: Auto-generated at `/swagger/v1` from docstrings and schemas
390+```python
391+from superset.mcp_service.chart.chart_utils import validate_chart_dataset
297392
298−### Migration Files
299−- **Location**: `superset/migrations/versions/`
300−- **Naming**: `YYYY-MM-DD_HH-MM_hash_description.py`
301−- **Utilities**: Use helpers from `superset.migrations.shared.utils` for database compatibility
302−- **Pattern**: Import utilities instead of raw SQLAlchemy operations
393+validation_result = validate_chart_dataset(chart, check_access=True)
394+if not validation_result.is_valid:
395+ await ctx.warning("Dataset not accessible: %s" % (validation_result.error,))
396+ return ChartError(
397+ error=validation_result.error or "Chart's dataset is not accessible",
398+ error_type="DatasetNotAccessible",
399+ )
400+```
303401
304−## Platform-Specific Instructions
402+Used by: `get_chart_info`, `get_chart_preview`, `get_chart_data`, `generate_chart`
305403
306−- **[CLAUDE.md](CLAUDE.md)** - For Claude/Anthropic tools
307−- **[.github/copilot-instructions.md](.github/copilot-instructions.md)** - For GitHub Copilot
308−- **[GEMINI.md](GEMINI.md)** - For Google Gemini tools
309−- **[GPT.md](GPT.md)** - For OpenAI/ChatGPT tools
310−- **[.cursor/rules/dev-standard.mdc](.cursor/rules/dev-standard.mdc)** - For Cursor editor
404+### 11. Compile Check for Chart Creation
311405
312−---
406+When creating, saving, or previewing charts, run schema validation (Tier 1)
407+and optionally a compile check (Tier 2) before persisting or caching.
408+``validate_and_compile`` glues both together; tools with tight SLAs
409+(``generate_explore_link``, ``update_chart_preview``) opt out of Tier 2.
313410
314−**LLM Note**: This codebase is actively modernizing toward full TypeScript and type safety. Always run `pre-commit run` to validate changes. Follow the ongoing refactors section to avoid deprecated patterns.
411+```python
412+from superset.mcp_service.chart.compile import validate_and_compile
413+
414+result = validate_and_compile(
415+ config, form_data, dataset, run_compile_check=True
416+)
417+if not result.success:
418+ # ``result.error_obj`` is a ``ChartGenerationError`` with fuzzy-match
419+ # suggestions ("did you mean sum_boys?") so the LLM can self-correct.
420+ ...
421+```
422+
423+The lower-level ``_compile_chart(form_data, dataset_id)`` is still exported
424+for callers that have already done their own schema validation.
425+
426+### 12. Flexible Input Parsing
427+
428+`ModelListCore` handles JSON string vs. native object parsing automatically via utilities in `superset.mcp_service.utils.schema_utils`:
429+
430+- `parse_json_or_passthrough(value, param_name)` - JSON string or dict
431+- `parse_json_or_list(value, param_name)` - JSON array, list, or comma-separated string
432+- `parse_json_or_model(value, model_class, param_name)` - JSON string or dict to Pydantic model
433+- `parse_json_or_model_list(value, model_class, param_name)` - JSON array to list of Pydantic models
434+
435+These are used internally by `ModelListCore` for `filters` and `select_columns`. Individual tools using core classes do NOT need to add parsing logic.
436+
437+## Middleware
438+
439+The MCP service uses FastMCP middleware (registered in `server.py`):
440+
441+- **`LoggingMiddleware`**: Logs tool calls with duration, entity IDs, sanitizes sensitive data
442+- **`GlobalErrorHandlerMiddleware`**: Catches unhandled exceptions, converts to ToolError
443+- **`StructuredContentStripperMiddleware`**: Strips structuredContent from responses (Claude.ai compatibility)
444+- **`ResponseSizeGuardMiddleware`**: Prevents oversized responses from crashing clients
445+- **`ResponseCachingMiddleware`**: Optional response caching (in-memory by default, Redis when store enabled)
446+
447+Middleware is applied in `server.py` and should NOT be modified in individual tools.
448+
449+## Configuration
450+
451+Default configuration is in `mcp_config.py`. Override in `superset_config.py`:
452+
453+```python
454+# Authentication
455+MCP_DEV_USERNAME = None # Fallback username for dev mode
456+MCP_AUTH_ENABLED = False # Enable JWT/API key auth
457+MCP_AUTH_FACTORY = None # Custom auth factory function
458+MCP_JWT_PUBLIC_KEY = None
459+MCP_JWT_SECRET = None
460+MCP_JWKS_URI = None
461+MCP_USER_RESOLVER = None # Custom function to extract username from JWT
462+
463+# RBAC
464+MCP_RBAC_ENABLED = True # Enable permission checking (default: True)
465+
466+# Embedded guest auth (opt-in; requires the EMBEDDED_SUPERSET feature flag).
467+# Reuses core GUEST_TOKEN_JWT_* config — no MCP-specific guest secret/audience.
468+MCP_EMBEDDED_GUEST_AUTH_ENABLED = False
469+# Default-deny: the ONLY tools a guest may call (everything else is denied).
470+MCP_GUEST_ALLOWED_TOOLS = {
471+ "get_dashboard_info", "get_dashboard_layout", "list_dashboards",
472+ "list_charts", "get_chart_info", "get_chart_data", "get_chart_preview",
473+}
474+# Principal-agnostic extension point: given the current user, return an allow-list
475+# (only these tools are callable) or None if unrestricted. Defaults to restricting
476+# embedded guests to MCP_GUEST_ALLOWED_TOOLS; override to add other restricted
477+# principals without touching the enforcement path.
478+MCP_RESTRICTED_TOOL_POLICY = None # Callable[[user], frozenset[str] | None]
479+
480+
481+# Response Caching (optional, uses in-memory store by default; Redis when MCP_STORE_CONFIG enabled)
482+MCP_CACHE_CONFIG = {
483+ "enabled": False,
484+ "list_tools_ttl": 300,
485+ "call_tool_ttl": 3600,
486+ "excluded_tools": ["execute_sql", "generate_dashboard"], # add tools to exclude
487+}
488+
489+# Multi-pod Storage (optional, requires Redis)
490+MCP_STORE_CONFIG = {
491+ "enabled": False,
492+ "CACHE_REDIS_URL": None,
493+ "event_store_ttl": 3600,
494+}
495+```
496+
497+## Testing Conventions
498+
499+### Test Organization
500+
501+Tests mirror the MCP service module structure:
502+```
503+tests/unit_tests/mcp_service/
504+├── conftest.py # Global fixtures (disable_mcp_rbac)
505+├── chart/
506+│ ├── test_chart_utils.py
507+│ ├── test_chart_schemas.py
508+│ └── tool/
509+│ ├── test_list_charts.py
510+│ ├── test_generate_chart.py
511+│ └── ...
512+├── dashboard/tool/
513+├── dataset/tool/
514+├── sql_lab/tool/
515+├── system/tool/
516+├── test_auth_*.py # Auth/RBAC tests
517+└── test_middleware*.py # Middleware tests
518+```
519+
520+### Async Tool Tests (primary pattern)
521+
522+```python
523+from unittest.mock import MagicMock, patch
524+import pytest
525+from fastmcp import Client
526+
527+from superset.mcp_service.app import mcp
528+from superset.utils import json
529+
530+@pytest.fixture
531+def mcp_server():
532+ return mcp
533+
534+@pytest.mark.asyncio
535+async def test_my_tool_success(mcp_server):
536+ mock_obj = MagicMock()
537+ mock_obj.id = 1
538+ mock_obj.name = "test"
539+
540+ with patch("superset.daos.chart.ChartDAO.find_by_id", return_value=mock_obj):
541+ async with Client(mcp_server) as client:
542+ result = await client.call_tool(
543+ "my_tool", {"request": {"id": 1}}
544+ )
545+ data = json.loads(result.content[0].text)
546+
547+ assert data["id"] == 1
548+```
549+
550+### Key Testing Patterns
551+
552+- **RBAC is disabled globally** via `conftest.py` autouse fixture (`MCP_RBAC_ENABLED = False`)
553+- **RBAC tests** are separate in `test_auth_rbac.py` with their own `enable_mcp_rbac` fixture
554+- **Auth is mocked** via `mock_auth` fixture that patches `get_user_from_request`
555+- **Mock objects** must have all attributes set explicitly (no auto-generation)
556+- **Patch at the DAO level**: `patch("superset.daos.chart.ChartDAO.find_by_id", ...)`
557+- **Schema validation tests** are synchronous (no Client needed)
558+
559+## Common Pitfalls to Avoid
560+
561+### 1. Forgetting Tool Import in app.py
562+**Problem**: Tool exists but isn't available to MCP clients.
563+**Solution**: Add tool import to `app.py` at the bottom (after `initialize_core_mcp_dependencies()`).
564+
565+### 2. Adding Tool Imports to server.py
566+**Problem**: Tools won't register properly.
567+**Solution**: Tool imports MUST be in `app.py`, not `server.py`.
568+
569+### 3. Wrong Decorator Import Path
570+**Problem**: Using stale import path.
571+**Solution**: Use `from superset_core.mcp.decorators import tool, ToolAnnotations` (NOT `superset_core.api.mcp`).
572+
573+### 4. Missing ToolAnnotations
574+**Problem**: Tool lacks MCP directory compliance metadata.
575+**Solution**: Always include `annotations=ToolAnnotations(title=..., readOnlyHint=..., destructiveHint=...)`.
576+
577+### 5. Using `Optional` Instead of Union Syntax
578+**Problem**: Old-style `Optional[T]` is not Python 3.10+ style.
579+**Solution**: Use `T | None` and `list[str]` instead of `Optional[T]` and `List[str]`.
580+
581+### 6. Direct Database Queries
582+**Problem**: Bypasses Superset's security and caching layers.
583+**Solution**: Use DAO classes (ChartDAO, DashboardDAO, DatasetDAO, DatabaseDAO).
584+
585+### 7. Not Using Core Classes
586+**Problem**: Duplicating list/get_info logic across tools.
587+**Solution**: Use `ModelListCore`, `ModelGetInfoCore`, `ModelGetSchemaCore`.
588+
589+### 8. Missing Apache License Headers
590+**Problem**: CI fails on license check.
591+**Solution**: Add ASF license header to all new `.py` files (see template at top of this doc).
592+
593+### 9. Circular Imports
594+**Problem**: Importing from `app.py` in tool files causes circular dependencies.
595+**Solution**: Use `from superset_core.mcp.decorators import tool` for tools/prompts. Only import `from superset.mcp_service.app import mcp` in resource files.
596+
597+### 10. Missing event_logger Instrumentation
598+**Problem**: Tool operations are invisible to observability.
599+**Solution**: Wrap key operations with `event_logger.log_context(action="mcp.tool_name.step")`.
600+
601+## Quick Checklist for New Tools
602+
603+- [ ] Created tool file in `{module}/tool/{tool_name}.py`
604+- [ ] Added ASF license header
605+- [ ] Used `@tool(tags=[...], class_permission_name="...", annotations=ToolAnnotations(...))` decorator
606+- [ ] Import: `from superset_core.mcp.decorators import tool, ToolAnnotations`
607+- [ ] Created Pydantic request/response schemas in `{module}/schemas.py`
608+- [ ] Used DAO classes instead of direct queries
609+- [ ] Added `event_logger.log_context()` instrumentation
610+- [ ] Used `await ctx.info/error/debug()` for context logging
611+- [ ] Exported from `{module}/tool/__init__.py`
612+- [ ] Added tool import to `app.py` at the bottom
613+- [ ] Created async unit tests in `tests/unit_tests/mcp_service/{module}/tool/`
614+- [ ] Updated `DEFAULT_INSTRUCTIONS` in `app.py` if adding new capability
615+
616+## Quick Checklist for New Prompts
617+
618+- [ ] Created prompt file in `{module}/prompts/{prompt_name}.py`
619+- [ ] Added ASF license header
620+- [ ] Used `@prompt("prompt_name")` from `superset_core.mcp.decorators`
621+- [ ] Made function async: `async def prompt_handler(...) -> str`
622+- [ ] Added import to `{module}/prompts/__init__.py`
623+- [ ] Verified module import exists in `app.py`
624+
625+## Quick Checklist for New Resources
626+
627+- [ ] Created resource file in `{module}/resources/{resource_name}.py`
628+- [ ] Added ASF license header
629+- [ ] Used `@mcp.resource("superset://{path}")` decorator
630+- [ ] Added `@mcp_auth_hook` decorator
631+- [ ] Added import to `{module}/resources/__init__.py`
632+- [ ] Verified module import exists in `app.py`
633+
634+## Getting Help
635+
636+- Check existing tool implementations for patterns (chart/tool/, dashboard/tool/)
637+- Review core classes in `mcp_core.py` for reusable functionality
638+- See `CLAUDE.md` in project root for general Superset development guidelines
639+- Consult Superset documentation: https://superset.apache.org/docs/
315640
