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]

Reply via email to