Skip to content

Latest commit

 

History

History
392 lines (305 loc) · 14.7 KB

File metadata and controls

392 lines (305 loc) · 14.7 KB

@mcp-apps-kit/ui-react-builder

npm node license

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.

Table of Contents

Background

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.

Features

  • 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 of defineReactUI calls) or widgetsDir (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-estree for 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 serverConfig option
  • Full compatibility with defineTool() from @mcp-apps-kit/core

Compatibility

  • Node.js: >= 18
  • Peer dependencies:
    • @mcp-apps-kit/core ^0.2.0
    • @mcp-apps-kit/ui-react ^0.2.0
    • react and react-dom ^18 || ^19
    • vite ^5 || ^6 || ^7 (optional, for Vite plugin)

Install

npm install @mcp-apps-kit/ui-react-builder

Usage

Quick start

Define 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}!`,
      }),
    }),
  },
});

Vite Plugin

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.

Discovery Modes

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.tsxget-weather.html).

HMR (Dev Mode)

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 (or plugin-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:

  • true or {} — 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.

How it works

Production (vite build):

  1. The plugin discovers widgets (via serverEntry AST scan or widgetsDir file scan)
  2. It resolves component imports to their source files
  3. Each component is bundled with React, ReactDOM, and @mcp-apps-kit/ui-react via esbuild
  4. Self-contained HTML files are written to outDir

Development (vite dev):

  1. The plugin discovers widgets and registers virtual modules (virtual:mcp-react-ui/<key>.tsx)
  2. Dev HTML files are written to outDir, containing Vite HMR client, React Refresh preamble, and a virtual module import
  3. Virtual modules resolve to entry-point code that mounts the widget component wrapped in AppsProvider
  4. 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 defineReactUI calls (e.g., inside defineTool)
  • Comments around definitions
  • Various import styles (named, default, aliased)
  • Complex code structures (conditionals, arrays, objects)

Supported patterns

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 name property:
    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.

Build commands

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"
  }
}

API

Definition Helpers

Export Description
defineReactUI Define a UI using a React component
isReactUIDef Type guard to check if a value is a ReactUIDef

defineReactUI Options

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)

Types

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

Build Functions

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.

Transform Utilities

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

HTML Utilities

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

Vite Plugin

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)

Custom logging

// 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

  • ../../examples/minimal - Simple hello world with React UI (serverEntry mode)
  • ../../examples/weather-app - Full-featured app with widgetsDir mode and HMR

Contributing

See ../../CONTRIBUTING.md for development setup and guidelines. Issues and pull requests are welcome.

License

MIT