justinpark opened a new issue, #43488: URL: https://github.com/apache/superset/issues/43488
## Motivation ### Background * **Current limitation:** [SIP-187](https://github.com/apache/superset/issues/35498) introduced Superset's MCP tools, but its focus was entirely backend tools. Many features are only controllable from the backend, which limits flexibility and UX on the frontend. * **Why frontend matters:** * Frontend tools give a responsive, real-time user experience. * They enable advanced visual interactions — live previews, immediately applying a setting to one element on screen. * That level of immediacy is difficult or impossible with backend-only tools. * Frontend tools let users work in draft mode and iterate without touching live data. * They can enhance an existing workflow incrementally, without a full backend implementation, for a tighter feedback loop. ### Benefits for Superset users * **Better collaboration with AI:** real-time UI feedback makes iteration between suggestion and fix faster and more intuitive. * **Faster decision-making:** instant previews and live updates help users build and validate dashboards, charts, and queries more efficiently. * **Greater customization:** users get more direct control over visual and functional details. ## Items Not Included This proposal (SIP) focuses on the **architecture of managing Superset client MCP tools and the mechanism for connecting Superset frontend actions to MCP tools.** Accordingly, the following items are outside of the scope of this proposal and will require separate discussion or implementation plans: * **Implementation of the MCP server Itself:** This SIP proposes how to interface Superset frontend features with the MCP tool, and it does not include the actual process of implementing the MCP server itself (e.g., communication with LLM, tool selection logic, etc.). * **Interface and Approval Process Between the Client Tool and MCP Service:** The external interface process, such as the client tool communicating with the MCP service to register or gain approval for tools (Tool Approval), is not covered in this proposal. This proposal only covers the process of defining and preparing the list of actions that the MCP tool can use within Superset. ## Proposed Change For frontend tools, the approach will be to define and reuse existing actions (i.e. redux actions) or APIs provided by @apache-superset/core. In this process, **functionalities that can be implemented through existing backend tools will(must) be excluded**. (For example, creating a dashboard will use existing backend tools. Frontend tools functions will include changing the layout or style of the dashboard currently visible on the screen, provide the ability to save these changes, or generate the necessary payload for the backend tool.) | ✅ Do | ❌ Don’t | | :---- | :---- | | Update the sql content | Save the sql content as a saved query (can via backend tool) | | Apply chart type and configuration changes | Add chart to a dashboard (can via backend tool) | | Change dashboard layout | Create a dashboard (can via backend tool) | | Trigger to refresh the list page | Collect the data by fetching dashboard list (can via backend tool) | # Frontend vs. Backend Tool Usage The implementation of Model Context Protocol (MCP) tools across both the frontend and backend of Superset is guided by the need to optimize user experience, performance, and reliability. The choice of where to implement a tool—client-side (frontend) or server-side (backend)—depends primarily on the nature of the action required. | Aspect | Frontend Tool | Backend Tool | | :---- | :---- | :---- | | **Primary Goal** | Real-time interactivity, visual updates, and user experience (UX) | Data processing, persistent changes, and complex system interactions | | **Execution Context** | Client-side (Browser) | Server-side (Superset Backend/Worker) | | **Network Dependency** | Low (actions on local UI state) | High (always requires a network request) | | **Reliability Concern** | Handles transient UI state; can fail due to client-side issues, but impact is local. | More reliable for critical, persistent changes. | | **When to Use** | Actions that **modify the current UI state without requiring a persistent save or complex data fetch** (e.g., live preview, layout adjustments, local filter manipulation). | Actions that **require server-side resources, persistence (database writes), external service communication, or significant data processing** (e.g., creating a new object, running a query, saving a dashboard). | **Use a frontend tool when** the action manipulates only what's *currently visible on screen* and needs to feel instant: 1. **Immediate UI updates** — layout changes, chart config tweaks, unsaved filter changes. 2. **State preparation** — building a payload (SQL, chart JSON) to hand off to a backend tool. 3. **Local context** — anything answerable from the client alone (current editor id, clipboard). **Use a backend tool when** the action needs the server, the database, or an external service: 1. **Server-side requests** — reading from the DB, writing to Superset's metadata DB, running a long query. 2. **Persistence** — creating, deleting, or permanently modifying any artifact (dashboards, charts, datasets, saved queries). 3. **Heavy processing** — anything that should run on server infrastructure, not the browser. ## Design ### `ClientTool` A client tool is a plain object — name, description, JSON Schema input, and a handler, and optional behavior hints, all together: ```ts interface ClientTool { name: string; description: string; inputSchema: Record<string, unknown>; handler: (input: unknown) => Promise<unknown> | unknown; annotations?: ClientToolAnnotations; } interface ClientToolAnnotations { readOnlyHint?: boolean; destructiveHint?: boolean; } ``` `inputSchema` is plain JSON Schema, not a Zod object — no `zod`/`zod-to-json-schema` dependency. There is no separate `outputSchema`; a handler's return value is reported back to the model as-is. `handler` lives on the tool itself — there's no separate `commands.registerCommand()`/`executeCommand()` indirection to invoke it. `annotations` is optional and, today, inert — nothing in `chat` reads or enforces it. Its two fields are named to match the backend MCP tools' own `readOnlyHint`/`destructiveHint` (`superset/mcp_service/*`'s `@tool(..., annotations=ToolAnnotations(...))`, from the official `mcp.types.ToolAnnotations`), so a too l that exists in both a client and backend form describes itself the same way in either one, and so a tool author can set them now rather than that being a breaking change to `ClientTool` once a consumer (e.g . a confirmation prompt before a destructive call) actually reads them. ### Registering tools An extension (or Superset core itself) registers its tools imperatively, mirroring `commands.registerCommand`/`chat.registerChat` — a direct call from the extension's own module, not a declarative file `extension.json` points at: ```ts import { chat } from '@apache-superset/core'; chat.registerClientTool({ name: 'dashboard__say_hi', description: 'Says hi to the user', inputSchema: { type: 'object', properties: {} }, handler: () => ({ success: true }), }); ``` `chat.registerClientTools(tools)` registers a whole list in one call — equivalent to mapping `registerClientTool` over the list, without writing that loop. Both return a `Disposable` that unregisters what they added. Grouping a surface's tools behind one function is an optional authoring convenience (`chat.ClientToolsFactory`, `(chat) => ClientTool[]`) — not a required contract. `chat.registerChat(chat, trigger, panel, options)` also takes tools directly, via an optional fourth `options.tools` argument — a shorthand for registering a chat and its tools together instead of in two calls: ```ts import { chat } from '@apache-superset/core'; chat.registerChat( { id: 'acme.chat', name: 'Acme Chat' }, AcmeTrigger, AcmePanel, { tools: [ { name: 'dashboard__say_hi', description: 'Says hi to the user', inputSchema: { type: 'object', properties: {} }, handler: () => ({ success: true }), }, ], }, ); ``` This is equivalent to calling `registerClientTools(options.tools)` right after `registerChat`, with both `Disposable`s bundled into the one `registerChat` returns — disposing it unregisters the chat and those tools together. Beyond that shared disposal, nothing changes: a tool registered this way is indistinguishable from one registered separately via `registerClientTool`/`registerClientTools` — same map, same duplicate-name handling, same per-extension prefixing, and the [send-time `getTools()` read](#consuming-tools-the-chat-extension) still doesn't care which call added it or in what order. There's no build-time wiring: no `extension.json` field, no separate Module Federation expose, no special-cased pre-loading step in the extension loader. A tool registers exactly when the extension's `./index` factory runs, the same as any other contribution type. ### Automatic per-extension prefixing A tool's `name` is written **without** any prefix — `dashboard__say_hi`, not `my-extension.dashboard__say_hi`. The host prepends the calling extension's id automatically. This works the same way `extensions.getContext()` already does: `ExtensionsLoader.ts` gives each extension's own Module Federation container a private, rebound copy of `@apache-superset/core`. For `chat`, that rebound copy wraps `registerClientTool`/`registerClientTools` so any call made through it gets `<extension-id>.` prepended to every tool name before the real registration happens. An extension's own code never sees or writes that prefix. Superset's own built-in tools go through a different path — `ExtensionsStartup.tsx` calls the unscoped `chat.registerClientTools()` directly at app startup, before any extension loads, and prefixes with `core.` explicitly: ```ts chat.registerClientTools( getCoreClientTools(chat).map(tool => ({ ...tool, name: `core.${tool.name}` })), ); ``` Because every extension id is globally unique and there is exactly one `"core"`, a name can never collide across two different registration sources. The only way to get a duplicate is a single source registering the same name twice — which logs a warning and **overwrites** the first, mirroring `commands.registerCommand`'s exact behavior. Nothing validates a tool's name beyond that; getting it right is on the tool's author, same as choosing a collision-free `Command.id`. ### Naming convention Not enforced, but every core tool and the chat extension's own tools follow it: ``` [surface]__[name] ``` Eight product surfaces are currently recognized by convention: | \# | Surface | | :---- | :---- | | 1 | dashboard | | 2 | chart | | 3 | sqlLab | | 4 | dataset | | 5 | alert | | 6 | report | | 7 | cssTemplate | | 8 | savedQuery | ### The registry (`ChatProvider`) `ChatProvider` (`superset-frontend/src/core/chat/ChatProvider.ts`) is the host-side singleton backing `chat`. Tools live in one flat `Map<string, ClientTool>`, keyed by each tool's final (already-qualified) name: * `registerClientTool(tool)` — sets it into the map (warn \+ overwrite on a duplicate key), returns a `Disposable` that removes it. * `registerClientTools(tools)` — maps `registerClientTool` over the list, bundles the `Disposable`s. * `registerChat(chat, trigger, panel, options?)` — when `options.tools` is given, calls `registerClientTools(options.tools)` internally and bundles its `Disposable` with the chat's own, so a single dispose call tears down both. * `getTools(format?)` — reads the map fresh on every call. It is never cached, since it can still be growing as extensions finish loading. ### Multi-format conversion (Future Plan) `chat.getTools()` with no argument returns the native `ClientTool[]` — each entry keeps its `handler`, so this is what a dispatcher should look tools up from by name. `chat.getTools(chat.ClientToolsFormat.Claude)` returns the same tools converted to the wire shape Anthropic's Messages API (and this codebase's own backend `ToolSpec`) expects: ```ts interface ClaudeToolSpec { name: string; description: string; input_schema: Record<string, unknown>; // renamed from inputSchema // handler is dropped — a wire format sent to an external API has no // business carrying a callable } ``` `ClientToolsFormat` is a plain `{ Claude, AgUi, CopilotKit, Codex }` `const` object, not a TS `enum` (the ambient package only ever *declares* — see its own docs for why a real `enum` wouldn't structurally match the separately-defined runtime object). Each format's conversion lives in one small lookup table next to `ChatProvider.getTools()`'s implementation — adding real support for a placeholder means replacing its one table entry, not introducing a class hierarchy. `AgUi`/`CopilotKit`/`Codex` exist as named members with **no real transform**: nothing in this codebase talks to any of those frameworks today (no installed package, no import, no provider extension), so calling `chat.getTools()` with one of them throws a clear "not yet implemented" error naming the exact format, rather than guessing at an unverified wire shape. `getTools()` also checks the requested format against the lookup table with `Object.hasOwn` before indexing — `chat` is reachable from untyped JS via `window.superset.chat`, so an unknown format string gets a named error instead of an opaque `undefined is not a function`. ### Consuming tools (the chat extension) ```ts async function handleSubmit(text: string) { // ... const clientTools = chat.getTools(); const clientToolSpecs = chat.getTools(chat.ClientToolsFormat.Claude); // ... } ``` A component that snapshotted `chat.getTools()` once at mount (e.g. via `useMemo(() => chat.getTools(), [])`) could permanently miss tools registered afterward, or tools from another extension still loading in the background. ## Candidate core actions {#candidate-core-actions} Candidate actions Superset core itself could expose, one table per product surface. Everything under `superset-frontend/src/core/clientTools/<surface>/`. ### Dashboard All six read/write real classic-dashboard Redux state via new functions on `@apache-superset/core`'s `dashboard` namespace (`getDashboardLayout`/`updateDashboardLayoutItem`, `getDashboardCss`/`updateDashboardCss`, `getDashboardFilters`/`updateDashboardFilter`) — no tool file reads or dispatches Redux directly. These are named `*DashboardLayout*`/`*DashboardCss*`/`*DashboardFilter*`, distinct from the "Dashboard v2" prototype canvas's own `updateLayout`/etc., to avoid colliding with those same-sounding but semantically different functions. Two correctness details worth knowing if you touch this code: * `updateDashboardLayoutItem`'s underlying Redux action (`UPDATE_COMPONENTS`) does a shallow **replace** of the whole node at that id, not a merge — a bare partial `meta` patch would silently drop the node's `type`, `children`, and any unrelated `meta` fields. The implementation reads the existing node and merges before dispatching, matching every real caller elsewhere in the dashboard codebase (e.g. `ChartHolder.tsx`'s `handleUpdateSliceName`). * `getDashboardFilters()`'s selector (`getAllActiveFilters`) needs four separate Redux slices assembled together (`chartConfiguration`, `nativeFilters`, `dataMask`, `allSliceIds`) — there's no single "all applied filters" selector already exported. It uses the exact selector `DashboardPage.tsx` itself uses for the same purpose. | Tool name | Description | Input | Output | | :---- | :---- | :---- | :---- | | `core.dashboard__get_active_id` | Get the ID of the currently active dashboard | `{}` | `{ dashboardId: number }` | | `core.dashboard__change_layout` | Change the layout of the current dashboard (grid size, element position) | `{ node_id: string, meta: object }` | `{ success: boolean, message: string }` | | `core.dashboard__update_style` | Change the theme/style of the current dashboard | `{ css: string }` | `{ success: boolean, message: string }` | | `core.dashboard__get_metadata` | Get the JSON payload underlying the entire dashboard layout schema | `{}` | `{ dashboardId: number, layout: object, css: string }` | | `core.dashboard__get_filters` | Get the currently applied filters on the dashboard | `{}` | `{ dashboardId: number, filters: object }` | | `core.dashboard__update_filters` | Change the filter settings of the dashboard | `{ filters: [{ filter_id: string, extra_form_data?: object, filter_state?: object }] }` | `{ success: boolean, message: string }` | Notes per tool: * **`get_active_id`** delegates to `dashboard.getDashboardId()`, backed by Redux's `dashboardInfo.id`. * **`change_layout`** merge-patches one node's `meta` via `updateDashboardLayoutItem()`; structural moves (reparenting/reordering) are out of scope. * **`update_style`** replaces the dashboard's whole CSS via `updateDashboardCss()`. * **`get_metadata`** combines `getDashboardLayout()` (keyed by node id, an object, not an array) and `getDashboardCss()`. * **`get_filters`** delegates to `getDashboardFilters()`. * **`update_filters`** applies each change via `updateDashboardFilter()` — the same action the filter bar dispatches on "Apply," so charts refetch immediately. Filter values are session-only; unlike layout/CSS, they don't persist on the next dashboard Save. ### SQL Lab — not implemented | Tool name | Description | | :---- | :---- | | `core.sqllab__get_current_editor_id` | Get the ID of the currently active SQL editor | | `core.sqllab__update_editor_sql` | Update the content of the active SQL editor | | `core.sqllab__run_current_query` | Run the query in the active SQL editor | | `core.sqllab__save_current_query_as_saved_query` | Save the current query as a saved query (may end up a backend tool instead) | ### Chart (Explore) — not implemented | Tool name | Description | | :---- | :---- | | `core.chart__update_config` | Change the visualization type and settings of the current chart | | `core.chart__apply_filter` | Apply or modify a filter on the current chart | | `core.chart__export_chart_data` | Export the current chart's data (CSV/JSON) | | `core.chart__view_query` | Get the SQL query underlying the chart | | `core.chart__get_form_data` | Get the JSON payload underlying the chart configuration | ### Dataset, Alert, Report, CSS Template, Saved Query — no candidates yet Each has an empty stub folder reserved under `superset-frontend/src/core/clientTools/<surface>/` — no actions proposed for these surfaces yet. ## Considerations * Registering a tool with no `inputSchema` is a type error at the call site (`ClientTool.inputSchema` is required) — caught at build time, not filtered out silently at runtime. * There is no pattern-based include/exclude filtering (à la webpack/Jest). Every registered tool — core's included — is exposed unconditionally; there's no per-deployment allow/deny list. * `chat.getTools()` returns whatever is currently registered regardless of the active page. A tool whose handler depends on page-specific state (e.g. `dashboard.getRoot()` on a page with no dashboard mounted) simply fails at call time if that state doesn't exist — no page-awareness filtering exists or is planned. * Duplicate tool names overwrite (with a logged warning), they don't get rejected — see [Automatic per-extension prefixing](#automatic-per-extension-prefixing). ## Roadmap 1. **Design schemas for [core actions](#candidate-core-actions)** — done for all six `dashboard` actions; the other seven surfaces are design-only. 2. **Implement handler code for core actions** — done for `dashboard`; the other seven surfaces are still empty stub folders. 3. **Migrate Redux actions that match a core action into extension-owned client tools** — not started. 4. **Document how to add more tool actions** in the developer portal — not started; this document is the closest thing to that today. * **Phase 1 (Q3 2026):** SIP approval; proof-of-concept implementation; core registration API design. * **Phase 2 (Q3 2026):** implement core actions across surfaces; support multiple agent-framework wire formats; documentation and community guidelines. * **Phase 3 (Q4 2026):** official release and continuous improvement. ### Migration Plan and Compatibility Not required ### Rejected Alternatives Describe alternative approaches that were considered and rejected. -- 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: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
