Skip to content

feat: interactive MCP Apps charts for create_chart in Claude and ChatGPT, compiled with Flint - #10016

Open
nishantmonu51 wants to merge 5 commits into
mainfrom
nishant/mcp-app-flint
Open

nishantmonu51 wants to merge 5 commits into
mainfrom
nishant/mcp-app-flint

Conversation

@nishantmonu51

@nishantmonu51 nishantmonu51 commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Renders create_chart results as interactive charts inside Claude and ChatGPT through MCP Apps, with the chart specs compiled by Flint.

  • The MCP server registers a ui://rill/widget/<hash>.html resource (a single-file Svelte + Vega bundle from the new web-mcp-app workspace, embedded in the binary) and links it from create_chart via _meta.ui. Hosts without MCP Apps support keep the markdown table and the "Open in Rill" link.
  • External clients call create_chart with a Flint chart_spec plus a Rill data binding (metrics view, time range, filter, grain). The server resolves the query through chartquery (per-channel top-N budgets, comparison folds) and returns the rows, field metadata, semantic types and the project theme. The deprecated chart_type + spec shape is still accepted for clients holding a cached tool list.
  • The widget compiles the spec with Flint on the client (the shared custom-viz/flint/compile.ts), follows the host theme (light/dark, fonts, style variables) and the project theme, and in fullscreen offers controls: time grain, top-N and comparison re-query through the app-only chart_data tool; chart type, layout and curve recompile locally. It reports its current view to the model via ui/update-model-context.
  • "Open in Rill" replays the call into an Explore; the /-/ai/.../-/open route now maps Flint-shaped calls as well.
  • Flint compile changes that canvas custom charts share: Rill's theme owns typography, colors and the color palette (Flint's config and tableau10 scheme no longer override it), multi-measure series are labeled with display names, row-faceted charts fill the box with upright row labels, derived axes take the measure's label and format, and discrete time axes are labeled at their grain.
  • A built-in rill-charts skill (distilled from Flint's authoring skill) tells external clients which chart fits which question and the pitfalls to avoid; it is always-apply, so it arrives in the ai_instructions of list_metrics_views, and rill init --agent writes it alongside the other skills.
  • Gated by chat_charts alone: it enables the chart tools and the widget, which every endpoint serves whenever the binary carries the bundle, and chart_data is advertised exactly where the widget is. chart_data calls count in the tool_call metric but are not persisted. Docs updated in docs/docs/guide/ai/mcp.md.

Tested with the Go suites (runtime/ai, chartquery goldens, runtime and admin MCP servers), the widget's vitest and Playwright fake-host suites plus its 650 KB size budget, the web-common Flint compile suite, and live in claude.ai (custom connector over a tunnel) and ChatGPT (custom MCP server), both in dark mode; light mode through the harness.

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

@nishantmonu51 nishantmonu51 added Type:Feature New feature request Area:AI AI assistant, chat and agents Size:XL Very large change: 2,000+ lines labels Oct 14, 2026

@djbarnwal djbarnwal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Architectural review findings

1. One tool name, two schemas

MCPSpecProvider lets create_chart advertise a different input schema on MCP than in-app, under the same name. Those arguments are persisted to the same session tables, so every consumer of stored messages has to handle both shapes. Two separate tool names, or one schema, would remove that problem.

2. Feature gating is spread across four mechanisms

  • chat_charts gates the tools.
  • mcp_apps and the mcp_apps_enabled annotation gate the widget on the runtime endpoint.
  • RILL_ADMIN_MCP_APPS_ENABLED gates the widget on the unified admin endpoint.
  • The hidden chart_data gate is the user-agent prefix plus host-side enforcement of visibility.

The admin and runtime endpoints already disagree about when chart_data exists

3. Widget-driven query cost

Calls to chart_data aren't counted as billable tool calls, aren't persisted, and have no rate limit. A user flipping controls quickly, or a non-UI client calling the tool directly, can generate unlimited metrics queries. Queries are still tagged with the mcp source, but it's worth making a deliberate decision about metering and throttling here.

Comment thread admin/server/mcp.go Outdated
func (s *Server) mcpHandler() (http.Handler, error) {
// The MCP Apps widget is served when enabled and the binary carries the widget bundle (see runtime/ai/mcpapp).
var widget *mcpapp.Asset
if s.opts.MCPAppsEnabled {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unified MCP server serves the widget and links create_chart to it based only on the RILL_ADMIN_MCP_APPS_ENABLED env var. The per-project mcp_apps flag and the mcp_apps_enabled annotation only apply to the direct runtime endpoint.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gone: the unified server serves the widget whenever the binary carries the bundle, with no env var, and the per-project flag and annotation are removed. chat_charts is the only gate left.

Comment thread runtime/ai/mcp.go
continue
}
t.RegisterWithMCPServer(srv)
t.RegisterWithMCPServer(srv, t.AppUI.Meta(widget))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The direct runtime MCP server registers chart_data for every external client whether or not the widget is served, and ChartData.CheckAccess doesn't check mcp_apps. App-only calls are also left out of the tool_call metric and aren't written to the session.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: chart_data is now registered only when the widget is served, on both endpoints, and app-only calls count in the tool_call metric. They stay unpersisted on purpose, since they are widget plumbing rather than conversation turns.

… Claude and ChatGPT

- Serve a self-contained chart widget (`web-mcp-app`, Svelte + Vega) as the MCP Apps resource `ui://rill/widget/<hash>.html` from the runtime and the admin unified MCP endpoints
- `create_chart` runs the chart query server-side for external MCP clients (`runtime/ai/chartquery`) and returns rows, fields, theme and `open_url` in `structuredContent`, with a markdown table fallback in `content`
- App-only `chart_data` tool re-queries for grain, top-N and comparison controls; view changes are pushed to the model with `ui/update-model-context`
- Gating: `chat_charts` for the tools, `mcp_apps` flag or `mcp_apps_enabled` annotation for the widget on the runtime endpoint, `RILL_ADMIN_MCP_APPS_ENABLED` on the admin endpoint
- Transient AI sessions for non-persisting MCP requests, Makefile `runtime.mcpapp.embed`, CI wiring, docs
External MCP clients now describe a chart with a Flint `chart_spec` and a
Rill `data` binding (metrics view, time range, filter, grain). The server
resolves the query through `chartquery`, with per-channel top-N budgets and
comparison folds, and returns the rows, field metadata, semantic types and
the project theme. The widget compiles the spec with Flint on the client
through the shared `custom-viz/flint/compile.ts`, re-queries grain, top-N
and comparison through the app-only `chart_data` tool, and recompiles chart
type, layout and curve locally. The deprecated `chart_type` + `spec` shape
stays accepted for clients holding a cached tool list, and "Open in Rill"
maps Flint-shaped calls into an Explore.

Flint compile changes shared with canvas custom charts: Rill's theme owns
typography, colors and the color palette (Flint's config and `tableau10`
scheme no longer override it); multi-measure series take the measures'
display names; row-faceted charts fill the box with upright row labels;
derived axes take the measure's label and format; discrete time axes are
labeled at their grain.
A built-in skill, distilled from Flint's authoring skill and tied to Rill's
`create_chart` arguments, says what to check before charting, which chart type
fits which question and the pitfalls that produce misleading charts (stacked
vs grouped bars, mixed units, too many categories, totals next to parts). It
is an always-apply skill for external clients, so Claude and ChatGPT receive it
in the `ai_instructions` of `list_metrics_views` without loading skills
themselves, and it is also served by `list_skills` / `load_skill`. Rill's own
agents keep their prompt guidance. `rill init --agent` writes the same file as
the `rill-charts` skill, and the analysis instructions point external agents
at it instead of the in-app chart types.
Collapse the `mcp_apps` feature flag, the `mcp_apps_enabled` annotation and
`RILL_ADMIN_MCP_APPS_ENABLED` into the existing `chat_charts` flag. The widget
is served wherever the binary carries the bundle, and both the runtime and the
unified endpoint advertise `chart_data` exactly when they serve the widget.

`chart_data` calls now count in the `tool_call` metric like any other data
tool while staying unpersisted. The widget debounces control changes and drops
stale `chart_data` responses. Also fix the three golangci-lint findings in
`runtime/ai/chartquery`.
The go-sdk v1.8 client defaults to a protocol whose handshake is a
`server/discover` request, which the runtime correctly treats as transient.
Claude and ChatGPT send `initialize`, which is what the session assertions in
`TestMCPApps` exercise.
@nishantmonu51

Copy link
Copy Markdown
Collaborator Author

Thanks, agree on all three.

  1. One schema per tool name is the end state, and Flint is it. The in-app chat still advertises the native shape because its renderer is built on the native chart components, so moving it is a frontend change of its own. I'll do that as the immediate follow-up PR and delete MCPSpecProvider with it. Today the handler accepts both shapes on every channel and the replay route handles both; the only single-shape consumer is the in-app chart block, which never sees Flint calls until that follow-up. Separate tool names would keep two shapes in the tables under different names, so I'd rather not.
  2. Collapsed to chat_charts. The widget is served wherever the bundle is embedded, and both endpoints now advertise chart_data exactly when they serve the widget. Removed mcp_apps, the annotation and RILL_ADMIN_MCP_APPS_ENABLED.
  3. The queries behind chart_data were already metered as mcp requests; only the tool_call metric skipped them. It now counts them like any other data tool, still unpersisted. It has the same exposure as query_metrics_view, so I'd treat throttling as an MCP-wide question rather than add a widget-specific limit. The widget now debounces control changes and drops stale responses.

Rebased on main as well, so CI runs again.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area:AI AI assistant, chat and agents Size:XL Very large change: 2,000+ lines Type:Feature New feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants