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:
- Execute the tool and get the result
- Fetch the linked UI component as an HTML resource
- 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
| Option | Type | Description |
|---|
component | React.ComponentType | string | Component or path to component file |
uri | string | Custom resource URI (auto-generated if not provided) |
title | string | HTML document title |
styles | string | Additional 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
| Option | Type | Description |
|---|
component | React.ComponentType | string | Component or path to component file |
uri | string | Custom resource URI |
title | string | HTML 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
| Prop | Type | Description |
|---|
appName | string | App name for identification |
children | ReactNode | App 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>;
}
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
| Property | Type | Description |
|---|
call | (args?) => Promise<any> | Execute the tool |
result | any | Tool result |
loading | boolean | Loading state |
error | Error | null | Error if any |
isConnected | boolean | Connection 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
- When a tool returns
_meta["mcp/www_authenticate"], ChatGPT displays an OAuth linking prompt
- The
useAuth hook detects this and updates isAuthenticated state
- Calling
login() triggers the OAuth flow by calling an auth-required tool
- After successful OAuth, ChatGPT automatically retries the tool call
Return Value
| Property | Type | Description |
|---|
isAuthenticated | boolean | Whether user is authenticated |
isLoading | boolean | Auth check in progress |
user | AuthUser | null | User info if authenticated |
error | Error | null | Auth error if any |
login | () => void | Trigger OAuth authentication |
logout | () => void | Clear auth state |
AuthUser Type
interface AuthUser {
sub?: string; // User ID
email?: string; // Email address
name?: string; // Display name
picture?: string; // Avatar URL
}
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
| Feature | ext-apps (AppProvider) | ChatGPT (GPTAppProvider) |
|---|
| Transport | PostMessage iframe | window.openai SDK |
| Theme sync | Full CSS variables | Basic light/dark |
| Tool calls | useTool, callTool | useGptTool |
| Resources | useResource | Not supported |
| Messages | useMessage | Not supported |
| Display modes | Inline, modal, fullscreen | Inline 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>
);
}