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 |
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>
);
}