Skip to main content

Decorators & GPT Apps

This page covers server-side decorators for linking tools to UI components, and ChatGPT-specific providers and hooks.

@UIApp Decorator

Links an MCP tool to a React UI component for ext-apps hosts (Claude Desktop, MCP-compatible hosts). When a tool decorated with @UIApp is called, the host will:
  1. Execute the tool and get the result
  2. Fetch the linked UI component as an HTML resource
  3. Render the UI in an iframe with the tool result

Basic Usage

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 }) {
    const weather = await fetchWeatherAPI(args.city);
    return {
      city: args.city,
      temperature: weather.temp,
      condition: weather.condition
    };
  }
}
Use a path string (e.g., './WeatherCard') for the component to avoid importing browser code in your server bundle. The CLI will resolve and build the component separately.

With Custom URI

@Tool({ description: 'Show dashboard' })
@UIApp({ 
  component: './DashboardView',
  uri: 'ui://analytics/dashboard',  // Custom resource URI
  title: 'Analytics Dashboard'       // HTML document title
})
async getDashboard() {
  return { /* dashboard data */ };
}

Options

OptionTypeDescription
componentReact.ComponentType | stringComponent or path to component file
uristringCustom resource URI (auto-generated if not provided)
titlestringHTML document title
stylesstringAdditional CSS styles

The UI Component

Your UI component receives the tool result via useToolResult:
// WeatherCard.tsx
import { AppProvider, useToolResult, Card, RequireConnection } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';

interface WeatherData {
  city: string;
  temperature: number;
  condition: string;
}

function WeatherCardContent() {
  const { result, loading } = useToolResult<WeatherData>();

  if (loading || !result) return <div>Loading...</div>;

  return (
    <Card>
      <h2>{result.city}</h2>
      <p className="text-4xl font-bold">{result.temperature}°C</p>
      <p>{result.condition}</p>
    </Card>
  );
}

export default function WeatherCard() {
  return (
    <AppProvider appInfo={{ name: 'WeatherCard', version: '1.0.0' }}>
      <RequireConnection>
        <WeatherCardContent />
      </RequireConnection>
    </AppProvider>
  );
}

@GPTApp Decorator

Links an MCP tool to a React UI component specifically for ChatGPT GPT Actions.
import { Tool } from '@leanmcp/core';
import { GPTApp } from '@leanmcp/ui';

class AnalyticsService {
  @Tool({ description: 'Show analytics dashboard' })
  @GPTApp({ component: './AnalyticsDashboard' })
  async getAnalytics(args: { period: string }) {
    return { /* analytics data */ };
  }
}

Options

OptionTypeDescription
componentReact.ComponentType | stringComponent or path to component file
uristringCustom resource URI
titlestringHTML document title

GPTAppProvider

Context provider for ChatGPT Apps using the native window.openai SDK.

Basic Setup

import { GPTAppProvider } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';

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

Props

PropTypeDescription
appNamestringApp name for identification
childrenReactNodeApp content

useGptApp

Access the GPT App context including connection state and theme.
import { useGptApp } from '@leanmcp/ui';

function MyComponent() {
  const { 
    isConnected,   // Whether connected to ChatGPT
    theme,         // 'light' | 'dark'
    displayMode,   // Display mode
    locale,        // User locale
    maxHeight,     // Max height from host
    callTool,      // Call a server tool
    error          // Connection error
  } = useGptApp();

  if (!isConnected) {
    return <div>Connecting to ChatGPT...</div>;
  }

  return <div>Connected! Theme: {theme}</div>;
}

useGptTool

Call MCP tools via ChatGPT’s SDK with loading and error states.

Basic Usage

import { useGptTool } from '@leanmcp/ui';

function DataViewer() {
  const { call, result, loading, error } = useGptTool('get-data');

  useEffect(() => {
    call({ id: '123' });
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return <pre>{JSON.stringify(result, null, 2)}</pre>;
}

Return Value

PropertyTypeDescription
call(args?) => Promise<any>Execute the tool
resultanyTool result
loadingbooleanLoading state
errorError | nullError if any
isConnectedbooleanConnection state

useAuth

Handle OAuth authentication flows in GPT Apps. This hook integrates with ChatGPT’s native OAuth linking UI.

Basic Usage

import { useAuth } from '@leanmcp/ui';

function ProtectedContent() {
  const { 
    isAuthenticated,  // Whether user is authenticated
    isLoading,        // Auth check in progress
    user,             // User info (if authenticated)
    error,            // Auth error (if any)
    login,            // Trigger OAuth flow
    logout,           // Clear auth state
  } = useAuth();

  if (isLoading) {
    return <div>Checking authentication...</div>;
  }

  if (!isAuthenticated) {
    return (
      <div>
        <p>Please sign in to continue</p>
        <button onClick={login}>Sign In</button>
      </div>
    );
  }

  return (
    <div>
      <p>Welcome, {user?.name}!</p>
      <button onClick={logout}>Sign Out</button>
    </div>
  );
}

How It Works

  1. When a tool returns _meta["mcp/www_authenticate"], ChatGPT displays an OAuth linking prompt
  2. The useAuth hook detects this and updates isAuthenticated state
  3. Calling login() triggers the OAuth flow by calling an auth-required tool
  4. After successful OAuth, ChatGPT automatically retries the tool call

Return Value

PropertyTypeDescription
isAuthenticatedbooleanWhether user is authenticated
isLoadingbooleanAuth check in progress
userAuthUser | nullUser info if authenticated
errorError | nullAuth error if any
login() => voidTrigger OAuth authentication
logout() => voidClear auth state

AuthUser Type

interface AuthUser {
  sub?: string;      // User ID
  email?: string;    // Email address
  name?: string;     // Display name
  picture?: string;  // Avatar URL
}

With Protected Tools

Combine useAuth with tools that require authentication:
import { useAuth, useGptTool, Button, Card } from '@leanmcp/ui';

function PrivateData() {
  const { isAuthenticated, user, login } = useAuth();
  const { call, result, loading } = useGptTool('get-private-data');

  if (!isAuthenticated) {
    return (
      <Card>
        <p>This feature requires authentication</p>
        <Button onClick={login}>Connect Account</Button>
      </Card>
    );
  }

  return (
    <Card>
      <p>Logged in as {user?.email}</p>
      <Button onClick={() => call()} disabled={loading}>
        {loading ? 'Loading...' : 'Fetch Private Data'}
      </Button>
      {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
    </Card>
  );
}

Server-Side Auth Errors

For the auth flow to work, your server tool should return MCP-compliant auth errors:
import { Tool, createAuthError } from '@leanmcp/core';

export class DataService {
  @Tool({ description: 'Get private user data' })
  async getPrivateData(args: any, meta?: any) {
    const token = meta?.authorization?.token;
    
    if (!token) {
      return createAuthError('Authentication required', {
        resourceMetadataUrl: `${process.env.PUBLIC_URL}/.well-known/oauth-protected-resource`,
        error: 'invalid_token',
      });
    }
    
    // Proceed with authenticated request...
  }
}

Environment Differences

ext-apps vs ChatGPT

Featureext-apps (AppProvider)ChatGPT (GPTAppProvider)
TransportPostMessage iframewindow.openai SDK
Theme syncFull CSS variablesBasic light/dark
Tool callsuseTool, callTooluseGptTool
ResourcesuseResourceNot supported
MessagesuseMessageNot supported
Display modesInline, modal, fullscreenInline only

Shared Features

Both environments support:
  • Tool execution
  • Theme awareness (light/dark)
  • Auto-applied styles via @leanmcp/ui/styles.css
  • Testing with MockAppProvider

Complete GPT App Example

import { GPTAppProvider, useGptTool, Card, Button } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';

function StockDashboard() {
  const { call, result, loading, error } = useGptTool('get-stock-price');

  return (
    <div className="p-4">
      <Button onClick={() => call({ symbol: 'AAPL' })} disabled={loading}>
        {loading ? 'Fetching...' : 'Get Apple Stock'}
      </Button>

      {error && (
        <div className="text-red-500 mt-2">Error: {error.message}</div>
      )}

      {result && (
        <Card className="mt-4">
          <h3>{result.symbol}</h3>
          <p className="text-2xl font-bold">${result.price}</p>
          <p className={result.change > 0 ? 'text-green-500' : 'text-red-500'}>
            {result.change > 0 ? '+' : ''}{result.change}%
          </p>
        </Card>
      )}
    </div>
  );
}

export default function App() {
  return (
    <GPTAppProvider appName="StockDashboard">
      <StockDashboard />
    </GPTAppProvider>
  );
}