This tutorial uses
@xpert-ai/plugin-echarts-mcp-app to explain MCP Apps from basic concepts to a realistic plugin implementation. The goal is to move beyond a chart demo and design an interactive sales performance app that runs inside ChatKit.Goal
We are not building a static chart. We are building an inline sales performance command center:- The user asks ChatKit to show 2026 revenue by region with drilldown analysis.
- The Agent calls the model-visible tool
echarts_sales_overview. - The tool returns a text summary, structured analysis data, and
_meta.ui.resourceUri. - ChatKit renders
ui://echarts-sales-dashboardas an inline iframe. - The user switches metric, year, and grouping, then clicks the chart to drill down.
- The iframe calls the app-only tool
echarts_sales_drilldownthrough the MCP Apps bridge. - The model does not see app-only tools, but the App can call them safely through the Xpert host.
Basic Concepts
MCP Apps combine three pieces:- MCP Tool: A callable tool. A model-visible tool usually opens the App and returns initial state.
- MCP Resource: An HTML resource served by the MCP server. MCP App resources normally use a
ui://...URI and must returntext/html;profile=mcp-app. - MCP Apps Host: In Xpert + ChatKit, the host reads the resource, creates a sandboxed iframe, injects initialization context, and proxies
tools/call/resources/read.
ui/initialize: the App initializes with the host and receives host capabilities and contextui/notifications/tool-input: the host sends the triggering tool inputui/notifications/tool-result: the host sends the initial tool resulttools/call: the App calls an allowed tool on the same MCP serverresources/read: the App reads an allowed MCP resourceui/notifications/size-changed: the App asks ChatKit to resize the iframe
Runtime Flow
The key point is that ChatKit does not persist arbitrary HTML in conversation history. It stores safe metadata such asresourceUri, toolName, toolsetId, and serverName, plus a size-limited initial tool result snapshot. When the page refreshes, the backend uses a still-live app instance first; if it has expired, it reads the ui:// resource again and revives the App instance.
Real Business Modeling
To make the sample plugin realistic, model it as a sales performance analysis capability instead of a drawing utility.
In production, the mock dataset can be replaced with CRM, ERP, warehouse, metric platform, or semantic model queries. Do not let the browser talk directly to sensitive databases. The iframe should call controlled MCP tools, and the backend should enforce authentication, tenant isolation, row-level permissions, and audit logging.
Plugin Layout
@xpert-ai/plugin-echarts-mcp-app uses this structure:
src/app is a normal Vanilla TypeScript frontend. The build writes dist/app/index.html; src/lib/app-html.ts only reads the built asset and returns it as an MCP resource.
Declare the Plugin-Managed MCP Server
The plugin declares its MCP server in.xpertai-plugin/plugin.json:
"${PLUGIN_ROOT}" matters. The installed runtime path changes whenever the plugin is installed or refreshed. Do not persist local development paths or a specific @runtime__... folder in the manifest or database.
Design Tool Visibility
Real MCP Apps usually have at least two types of tools:- Model-visible tools: tools the Agent can choose, such as
echarts_sales_overview - App-only tools: tools only the iframe can call for refresh, drilldown, pagination, or export preflight, such as
echarts_sales_drilldown
Return Structured Business Results
The MCP App should not parse natural language summaries. Tool results should provide:content: text for the model and non-UI clientsstructuredContent: stable JSON for the App_meta.ui.resourceUri: the App resource ChatKit should render
sales-performance-analysis.v1 and keep the App compatible with at least one previous version. That prevents old conversation messages from breaking when backend analysis evolves.
Also keep initial result size under control. ChatKit reads the complete toolResult from a live app instance first, but chat history only inlines small result snapshots; oversized results store only size and truncation metadata. For sales performance analysis, keep initial structuredContent to first-screen aggregates, summaries, and required filters. Load long details, large lists, and deeper levels through app-only tools with pagination or drilldown calls.
Register the MCP App Resource
The App HTML is returned by an MCP resource. Security and rendering metadata belongs on the resource, not the tool:https://cdn.jsdelivr.net in the resource CSP. Avoid broad * domains. If a production App can bundle third-party libraries offline, do that; the CSP becomes simpler and more predictable.
Author the App as Frontend Source
Do not maintain production App HTML as a huge TypeScript template string. Keep it as normal frontend source:scripts/build-app.mjs uses esbuild to bundle browser TypeScript and inline CSS and JS into one HTML file:
dist/app/index.html. This lets frontend developers maintain DOM, styles, state, and interactions in a familiar way. Tests can also inspect the built asset for bridge calls such as ui/initialize and tools/call.
App Bridge Lifecycle
When the App starts, initialize with the host and then wait for the host to send the initial tool input and result:ui/notifications/tool-result, it extracts structuredContent.analysis and renders the chart. When the user clicks the chart, it calls the app-only tool:
ui/notifications/size-changed so ChatKit can keep the iframe height aligned with the content.
Build and Validate
Build the App asset first, then compile the plugin server:nx build @xpert-ai/plugin-echarts-mcp-app runs both the server build and the App build.
Before shipping, verify:
- the stdio MCP server starts without writing normal logs to stdout
tools/listincludes the model-visibleecharts_sales_overview- the app-only
echarts_sales_drilldownis not exposed to the model - the overview tool result includes
_meta.ui.resourceUri - the resource returns
text/html;profile=mcp-app - the resource metadata includes a minimal CSP
- the iframe receives initial tool input and tool result
- chart clicks call
tools/calland return drilldown data - refresh recovery does not persist or leak raw HTML
From Example to Production
Use these upgrades when turning the demo into a real business App:
A mature MCP App plugin is more than HTML plus charts. It should design business semantics, tool permissions, interaction state, auditability, and fallback behavior together.
FAQ
Why not use ChatKit Widgets? Widgets are best for controlled declarative UI. MCP Apps are better when the plugin owns a full HTML app with complex state, third-party visualization libraries, chart interactions, or repeated app-only tool calls. Why not use extension views? Extension views are platform surfaces for persistent workbench pages, configuration screens, and integration detail pages. MCP Apps are tool-call results whose lifecycle follows the conversation. Why should the model not see every drilldown tool? Drilldown, pagination, and refresh tools are UI implementation details. Making them app-only keeps the model tool list smaller and reduces accidental calls. Is HTML stored in conversation history? No. Xpert stores safe metadata only. The HTML is read again by the MCP Apps Host at render time.Next Step
Start from@xpert-ai/plugin-echarts-mcp-app, replace the sample sales data with your own business query, and evolve the ECharts view into the analysis surface your users actually need. As long as the boundaries between MCP tools, MCP resources, and the standard bridge stay clear, the same plugin can deliver a rich business application directly inside ChatKit.