Build tool for React-based MCP application UIs.
@mcp-apps-kit/ui-react-builder allows you to define UI resources using React components instead of pre-built HTML files. The framework handles bundling React, ReactDOM, and @mcp-apps-kit/ui-react into self-contained HTML that works with both MCP Apps and ChatGPT.
Building interactive UI widgets for MCP applications traditionally requires manually bundling React components into self-contained HTML files. This package automates that process, letting you define UIs with React components directly in your tool definitions.
defineReactUI()helper for type-safe React component definitions- Vite plugin for automatic discovery and building of React UIs
- Two discovery modes:
serverEntry(AST-based scan ofdefineReactUIcalls) orwidgetsDir(file-based scan of a widgets directory) - HMR with React Fast Refresh — edit widgets and see changes instantly via Vite's dev server
- AST-based parsing using
@typescript-eslint/typescript-estreefor reliable detection - esbuild-powered bundling to self-contained HTML (production)
- Auto-generated HTML paths from component names (kebab-case)
- Global CSS injection support with PostCSS/Tailwind processing
- Configurable logging for plugin output
- Server configuration injection via
serverConfigoption - Full compatibility with
defineTool()from@mcp-apps-kit/core
- Node.js:
>= 18 - Peer dependencies:
@mcp-apps-kit/core^0.2.0@mcp-apps-kit/ui-react^0.2.0reactandreact-dom^18 || ^19vite^5 || ^6 || ^7(optional, for Vite plugin)
npm install @mcp-apps-kit/ui-react-builderDefine your React component:
// src/ui/GreetingWidget.tsx
import { useToolResult, useHostContext } from "@mcp-apps-kit/ui-react";
export function GreetingWidget() {
const result = useToolResult();
const { theme } = useHostContext();
return (
<div data-theme={theme}>
<h1>{result?.greet?.message}</h1>
</div>
);
}Use defineReactUI in your tool definition:
// src/index.ts
import { createApp, defineTool } from "@mcp-apps-kit/core";
import { defineReactUI } from "@mcp-apps-kit/ui-react-builder";
import { GreetingWidget } from "./ui/GreetingWidget";
import { z } from "zod";
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
greet: defineTool({
description: "Greet someone",
input: z.object({ name: z.string() }),
output: z.object({ message: z.string() }),
ui: defineReactUI({
component: GreetingWidget,
name: "Greeting Widget",
prefersBorder: true,
// Optional: Disable automatic size notifications (default: true)
// autoResize: false,
}),
handler: async ({ name }) => ({
message: `Hello, ${name}!`,
}),
}),
},
});The Vite plugin automatically discovers React UI components and builds them into self-contained HTML files. In dev mode it serves widgets through Vite's dev server with HMR and React Fast Refresh.
The plugin supports two mutually exclusive discovery modes:
serverEntry mode — scans a server entry file for defineReactUI calls using AST parsing:
// vite.config.ts
import { defineConfig } from "vite";
import { mcpReactUI } from "@mcp-apps-kit/ui-react-builder/vite";
export default defineConfig({
plugins: [
mcpReactUI({
serverEntry: "./src/index.ts",
outDir: "./src/ui/dist",
globalCss: "./src/ui/styles.css",
}),
],
});widgetsDir mode — scans a directory for .tsx files that export a default React component and a ui metadata object:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { mcpReactUI } from "@mcp-apps-kit/ui-react-builder/vite";
export default defineConfig({
plugins: [
react(),
mcpReactUI({
widgetsDir: "./ui/widgets",
outDir: "./ui/dist",
globalCss: "./ui/styles.css",
standalone: true,
}),
],
});Widget files in widgetsDir should follow this pattern:
// ui/widgets/get-weather.tsx
import { useToolResult } from "@mcp-apps-kit/ui-react";
import type { WidgetMetadata } from "@mcp-apps-kit/core";
export default function WeatherWidget() {
const result = useToolResult<{ temperature: number }>();
return <div>{result?.temperature}°C</div>;
}
export const ui: WidgetMetadata = {
name: "Weather Display",
prefersBorder: true,
};The HTML output path is inferred from the file name (e.g., get-weather.tsx → get-weather.html).
When Vite runs in serve mode (vite dev), the plugin generates lightweight HTML files that load widgets through Vite's dev server with React Fast Refresh instead of bundling with esbuild. This gives you instant feedback on widget changes without full rebuilds.
Requirements:
@vitejs/plugin-react(orplugin-react-swc) must be in your Vite config for Fast Refresh to work. The plugin will log a warning if it's missing.
mcpReactUI({
widgetsDir: "./ui/widgets",
// Enable HMR with custom dev server URL (useful when MCP server and Vite are on different ports)
dev: {
baseUrl: "http://localhost:5173",
},
});The dev option accepts:
trueor{}— enable with defaults (automatic in serve mode)false— disable HMR even in serve mode (always use esbuild)DevServerOptions— enable with custom configuration
When the MCP server and Vite dev server run on different ports, set dev.baseUrl to the Vite dev server origin so /@vite/client and virtual module imports resolve correctly.
Production (vite build):
- The plugin discovers widgets (via
serverEntryAST scan orwidgetsDirfile scan) - It resolves component imports to their source files
- Each component is bundled with React, ReactDOM, and
@mcp-apps-kit/ui-reactvia esbuild - Self-contained HTML files are written to
outDir
Development (vite dev):
- The plugin discovers widgets and registers virtual modules (
virtual:mcp-react-ui/<key>.tsx) - Dev HTML files are written to
outDir, containing Vite HMR client, React Refresh preamble, and a virtual module import - Virtual modules resolve to entry-point code that mounts the widget component wrapped in
AppsProvider - Vite serves and transforms the modules on the fly with HMR support
The serverEntry mode uses @typescript-eslint/typescript-estree for reliable AST-based detection of imports and defineReactUI calls. This is more robust than regex-based parsing and correctly handles:
- Nested
defineReactUIcalls (e.g., insidedefineTool) - Comments around definitions
- Various import styles (named, default, aliased)
- Complex code structures (conditionals, arrays, objects)
The plugin discovers defineReactUI calls using static analysis. For reliable detection:
- Import components directly from their source files:
import { MyWidget } from "./ui/MyWidget"; // ✓ Works import { MyWidget } from "./ui"; // ✗ Barrel imports not supported
- Use string literals for the
nameproperty:name: "My Widget"; // ✓ Works name: `My ${type}`; // ✗ Template literals not supported
- Reference components by identifier:
component: MyWidget; // ✓ Works component: widgets.MyWidget; // ✗ Property access not supported
If you need patterns not supported by auto-discovery, use defineUI({ html: "..." }) with manual Vite bundling.
With HMR (recommended for development):
{
"scripts": {
"dev": "concurrently \"pnpm dev:server\" \"pnpm dev:ui\"",
"dev:server": "tsx watch src/index.ts",
"dev:ui": "vite dev",
"build": "pnpm build:ui && tsc",
"build:ui": "vite build"
}
}Without HMR (esbuild-only workflow):
{
"scripts": {
"dev": "concurrently \"pnpm dev:server\" \"pnpm dev:ui\"",
"dev:server": "tsx watch src/index.ts",
"dev:ui": "vite build --watch",
"build": "pnpm build:ui && tsc",
"build:ui": "vite build"
}
}| Export | Description |
|---|---|
defineReactUI |
Define a UI using a React component |
isReactUIDef |
Type guard to check if a value is a ReactUIDef |
| Option | Type | Default | Description |
|---|---|---|---|
component |
ComponentType |
(required) | React component to render |
name |
string |
(required) | Display name for the UI |
description |
string |
- | Description of the UI widget |
prefersBorder |
boolean |
- | Hint to the host whether a border should be drawn |
autoResize |
boolean |
true |
Enable automatic size change notifications. Only supported in MCP Apps; ignored in ChatGPT. |
csp |
CSPConfig |
- | Content Security Policy configuration (ChatGPT only) |
| Type | Description |
|---|---|
ReactUIInput |
Input type for defineReactUI() |
ReactUIDef |
Output type (extends UIDef from core) |
BuildOptions |
Options for the build process |
BuildResult |
Result of building React UIs |
PluginLogger |
Logger interface for Vite plugin |
DevHTMLOptions |
Options for generateDevHTML() |
DevServerOptions |
HMR/dev server configuration |
| Export | Description |
|---|---|
buildReactUIs |
Build multiple React UIs to HTML |
buildReactUI |
Build a single React UI to HTML |
Note: The programmatic build functions (
buildReactUIs,buildReactUI) serialize components using.toString(), which has limitations:
- No external imports (components cannot import other modules)
- No closures (components that capture external variables won't work)
- Simple components only (best for self-contained components)
For production use, prefer the Vite plugin which uses file paths for proper import resolution.
| Export | Description |
|---|---|
transformToCoreDefs |
Convert ReactUIDefs to standard UIDefs |
transformSingleToCoreDef |
Convert a single ReactUIDef to UIDef |
extractReactUIs |
Separate React UIs from standard UIs |
buildAndTransform |
Build and transform in one step |
| Export | Description |
|---|---|
generateHTML |
Generate production HTML document from bundled JS |
generateDevHTML |
Generate dev-mode HTML with Vite HMR support |
generateEntryPoint |
Generate React entry point code |
import { mcpReactUI } from "@mcp-apps-kit/ui-react-builder/vite";| Option | Type | Default | Description |
|---|---|---|---|
serverEntry |
string |
- | Server entry point to scan (mutually exclusive with widgetsDir) |
widgetsDir |
string |
- | Directory of widget .tsx files (mutually exclusive with serverEntry) |
outDir |
string |
"./dist/ui" |
Output directory for HTML files |
minify |
boolean |
true in prod |
Minify output JavaScript |
globalCss |
string |
- | Path to global CSS file (processed through PostCSS if available) |
logger |
PluginLogger | false |
console | Custom logger or false to disable logging |
standalone |
boolean |
false |
Take over Vite build (emit only UI HTML) |
serverConfig |
McpServerConfig |
- | Server config injected into UIs at build time |
dev |
DevServerOptions | boolean |
auto | HMR/dev server options (auto-enabled in serve mode) |
// Disable all logging
mcpReactUI({
serverEntry: "./src/index.ts",
logger: false,
});
// Custom logger
mcpReactUI({
serverEntry: "./src/index.ts",
logger: {
info: (msg) => myLogger.info(msg),
warn: (msg) => myLogger.warn(msg),
error: (msg) => myLogger.error(msg),
},
});../../examples/minimal- Simple hello world with React UI (serverEntrymode)../../examples/weather-app- Full-featured app withwidgetsDirmode and HMR
See ../../CONTRIBUTING.md for development setup and guidelines. Issues and pull requests are welcome.
MIT