Skip to main content

@leanmcp/ui

Build rich, interactive MCP Apps with React components designed for the Model Context Protocol. Features first-class tool integration, streaming support, and automatic host theming.

Features

MCP-Native Components

ToolButton, ToolDataGrid, ToolForm - components that directly integrate with MCP tools

Dual Platform Support

Works with ext-apps hosts (Claude Desktop) and ChatGPT GPT Actions

Host Theming

Automatic theme sync with host application (light/dark mode)

Testing Utilities

MockAppProvider for unit testing MCP App components

Installation

npm install @leanmcp/ui
Import the styles in your app entry point:
import '@leanmcp/ui/styles.css';

Two App Paradigms

@leanmcp/ui supports two different host environments:

ext-apps (Claude Desktop, MCP Hosts)

Uses the @modelcontextprotocol/ext-apps protocol for iframe-based communication.
import { AppProvider, ToolButton } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';

function MyApp() {
  return (
    <AppProvider appInfo={{ name: 'MyApp', version: '1.0.0' }}>
      <ToolButton tool="refresh-data" resultDisplay="toast">
        Refresh Data
      </ToolButton>
    </AppProvider>
  );
}

ChatGPT GPT Actions

Uses ChatGPT’s native window.openai SDK.
import { GPTAppProvider, useGptTool } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';

function MyGPTApp() {
  return (
    <GPTAppProvider appName="MyApp">
      <DataDisplay />
    </GPTAppProvider>
  );
}

function DataDisplay() {
  const { call, result, loading } = useGptTool('get-data');
  
  return (
    <button onClick={() => call()} disabled={loading}>
      {loading ? 'Loading...' : 'Fetch Data'}
    </button>
  );
}

Quick Start Example

Here’s a complete example showing a tool-linked UI component:
import { AppProvider, ToolDataGrid, RequireConnection } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';

function UsersApp() {
  return (
    <AppProvider appInfo={{ name: 'UsersApp', version: '1.0.0' }}>
      <RequireConnection>
        <ToolDataGrid
          dataTool="list-users"
          columns={[
            { key: 'name', header: 'Name', sortable: true },
            { key: 'email', header: 'Email' },
            { key: 'status', header: 'Status' }
          ]}
          transformData={(result) => ({
            rows: result.users,
            total: result.total
          })}
          rowActions={[
            { label: 'Edit', tool: 'edit-user' },
            { label: 'Delete', tool: 'delete-user', variant: 'destructive' }
          ]}
          pagination
        />
      </RequireConnection>
    </AppProvider>
  );
}

Server-Side Integration

Link UI components to tools using the @UIApp decorator (for ext-apps) or @GPTApp decorator (for ChatGPT):
import { Tool } from '@leanmcp/core';
import { UIApp } from '@leanmcp/ui';

class WeatherService {
  @Tool({ description: 'Get weather for a city' })
  @UIApp({ component: './WeatherCard' })
  async getWeather(args: { city: string }) {
    return { city: args.city, temperature: 22, condition: 'Sunny' };
  }
}
When the tool is called, the host will render the linked UI component with the tool result.

Testing

Use MockAppProvider for unit testing:
import { MockAppProvider } from '@leanmcp/ui/testing';
import { render, screen } from '@testing-library/react';

test('WeatherCard displays temperature', () => {
  render(
    <MockAppProvider 
      toolResult={{ city: 'London', temperature: 20 }}
      isConnected={true}
    >
      <WeatherCard />
    </MockAppProvider>
  );
  expect(screen.getByText('20°C')).toBeInTheDocument();
});

What’s Next