Awesome-omni-skill chatgpt-app-builder

Build ChatGPT apps with interactive widgets using mcp-use and OpenAI Apps SDK. Use when creating ChatGPT apps, building MCP servers with widgets, defining React widgets, working with Apps SDK, or when user mentions ChatGPT widgets, mcp-use widgets, or Apps SDK development.

install
source · Clone the upstream repo
git clone https://github.com/diegosouzapw/awesome-omni-skill
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/chatgpt-app-builder-neversight" ~/.claude/skills/diegosouzapw-awesome-omni-skill-chatgpt-app-builder-9ac98d && rm -rf "$T"
manifest: skills/development/chatgpt-app-builder-neversight/SKILL.md
source content

ChatGPT App Builder

Build production-ready ChatGPT apps with interactive widgets using the mcp-use framework and OpenAI Apps SDK. This skill provides zero-config widget development with automatic registration and built-in React hooks.

Quick Start

Always bootstrap with the Apps SDK template:

npx create-mcp-use-app my-chatgpt-app --template apps-sdk
cd my-chatgpt-app
yarn install
yarn dev

This creates a project structure:

my-chatgpt-app/
├── resources/              # React widgets (auto-registered!)
│   ├── display-weather.tsx # Example widget
│   └── product-card.tsx    # Another widget
├── public/                 # Static assets
│   └── images/
├── index.ts               # MCP server entry
├── package.json
├── tsconfig.json
└── README.md

Why mcp-use for ChatGPT Apps?

Traditional OpenAI Apps SDK requires significant manual setup:

  • Separate project structure (server/ and web/ folders)
  • Manual esbuild/webpack configuration
  • Custom useWidgetState hook implementation
  • Manual React mounting code
  • Manual CSP configuration
  • Manual widget registration

mcp-use simplifies everything:

  • ✅ Single command setup
  • ✅ Drop widgets in
    resources/
    folder - auto-registered
  • ✅ Built-in
    useWidget()
    hook with state, props, tool calls
  • ✅ Automatic bundling with hot reload
  • ✅ Automatic CSP configuration
  • ✅ Built-in Inspector for testing

Creating Widgets

Simple Widget (Single File)

Create

resources/weather-display.tsx
:

import { McpUseProvider, useWidget, type WidgetMetadata } from 'mcp-use/react';
import { z } from 'zod';

// Define widget metadata
export const widgetMetadata: WidgetMetadata = {
  description: 'Display current weather for a city',
  props: z.object({
    city: z.string().describe('City name'),
    temperature: z.number().describe('Temperature in Celsius'),
    conditions: z.string().describe('Weather conditions'),
    humidity: z.number().describe('Humidity percentage'),
  }),
};

const WeatherDisplay: React.FC = () => {
  const { props, isPending } = useWidget();
  
  // Always handle loading state first
  if (isPending) {
    return (
      <McpUseProvider autoSize>
        <div className="animate-pulse p-4">Loading weather...</div>
      </McpUseProvider>
    );
  }
  
  return (
    <McpUseProvider autoSize>
      <div className="weather-card p-4 rounded-lg shadow">
        <h2 className="text-2xl font-bold">{props.city}</h2>
        <div className="temp text-4xl">{props.temperature}°C</div>
        <p className="conditions">{props.conditions}</p>
        <p className="humidity">Humidity: {props.humidity}%</p>
      </div>
    </McpUseProvider>
  );
};

export default WeatherDisplay;

That's it! The widget is automatically:

  • Registered as MCP tool
    weather-display
  • Registered as MCP resource
    ui://widget/weather-display.html
  • Bundled for Apps SDK compatibility
  • Ready to use in ChatGPT

Complex Widget (Folder Structure)

For widgets with multiple components:

resources/
└── product-search/
    ├── widget.tsx          # Entry point (required name)
    ├── components/
    │   ├── ProductCard.tsx
    │   └── FilterBar.tsx
    ├── hooks/
    │   └── useFilter.ts
    ├── types.ts
    └── constants.ts

Entry point (

widget.tsx
):

import { McpUseProvider, useWidget, type WidgetMetadata } from 'mcp-use/react';
import { z } from 'zod';
import { ProductCard } from './components/ProductCard';
import { FilterBar } from './components/FilterBar';

export const widgetMetadata: WidgetMetadata = {
  description: 'Display product search results with filtering',
  props: z.object({
    products: z.array(z.object({
      id: z.string(),
      name: z.string(),
      price: z.number(),
      image: z.string(),
    })),
    query: z.string(),
  }),
};

const ProductSearch: React.FC = () => {
  const { props, isPending, state, setState } = useWidget();
  
  if (isPending) {
    return <McpUseProvider autoSize><div>Loading...</div></McpUseProvider>;
  }
  
  return (
    <McpUseProvider autoSize>
      <div>
        <h1>Search: {props.query}</h1>
        <FilterBar onFilter={(filters) => setState({ filters })} />
        <div className="grid grid-cols-3 gap-4">
          {props.products.map(p => (
            <ProductCard key={p.id} product={p} />
          ))}
        </div>
      </div>
    </McpUseProvider>
  );
};

export default ProductSearch;

Widget Metadata

Required metadata for automatic registration:

export const widgetMetadata: WidgetMetadata = {
  // Required: Human-readable description
  description: 'Display weather information',
  
  // Required: Zod schema for widget props
  props: z.object({
    city: z.string().describe('City name'),
    temperature: z.number(),
  }),
  
  // Optional: Disable automatic tool registration
  exposeAsTool: true, // default
  
  // Optional: Apps SDK metadata
  appsSdkMetadata: {
    'openai/widgetDescription': 'Interactive weather display',
    'openai/toolInvocation/invoking': 'Loading weather...',
    'openai/toolInvocation/invoked': 'Weather loaded',
    'openai/widgetCSP': {
      connect_domains: ['https://api.weather.com'],
      resource_domains: ['https://cdn.weather.com'],
    },
  },
};

Important:

  • description
    : Used for tool and resource descriptions
  • props
    : Zod schema defines widget input parameters
  • exposeAsTool
    : Set to
    false
    if only using widget via custom tools
  • Default Apps SDK metadata is auto-generated if not specified

useWidget Hook

The

useWidget
hook provides everything you need:

const {
  // Widget props from tool input
  props,
  
  // Loading state (true = tool still executing)
  isPending,
  
  // Persistent widget state
  state,
  setState,
  
  // Theme from host (light/dark)
  theme,
  
  // Call other MCP tools
  callTool,
  
  // Display mode control
  displayMode,
  requestDisplayMode,
  
  // Additional tool output
  output,
} = useWidget<MyPropsType, MyOutputType>();

Props and Loading States

Critical: Widgets render BEFORE tool execution completes. Always handle

isPending
:

const { props, isPending } = useWidget<WeatherProps>();

// Pattern 1: Early return
if (isPending) {
  return <div>Loading...</div>;
}
// Now props are safe to use

// Pattern 2: Conditional rendering
return (
  <div>
    {isPending ? (
      <LoadingSpinner />
    ) : (
      <div>{props.city}</div>
    )}
  </div>
);

// Pattern 3: Optional chaining (partial UI)
return (
  <div>
    <h1>{props.city ?? 'Loading...'}</h1>
  </div>
);

Widget State

Persist data across widget interactions:

const { state, setState } = useWidget();

// Save state (persists in ChatGPT localStorage)
const addFavorite = async (city: string) => {
  await setState({
    favorites: [...(state?.favorites || []), city]
  });
};

// Update with function
await setState(prev => ({
  ...prev,
  count: (prev?.count || 0) + 1
}));

Calling MCP Tools

Widgets can call other tools:

const { callTool } = useWidget();

const refreshData = async () => {
  try {
    const result = await callTool('get-weather', {
      city: 'Tokyo'
    });
    console.log('Result:', result.content);
  } catch (error) {
    console.error('Tool call failed:', error);
  }
};

Display Mode Control

Request different display modes:

const { displayMode, requestDisplayMode } = useWidget();

const goFullscreen = async () => {
  await requestDisplayMode('fullscreen');
};

// Current mode: 'inline' | 'pip' | 'fullscreen'
console.log(displayMode);

Custom Tools with Widgets

Create tools that return widgets:

import { MCPServer, widget, text } from 'mcp-use/server';
import { z } from 'zod';

const server = new MCPServer({
  name: 'weather-app',
  version: '1.0.0',
});

server.tool({
  name: 'get-weather',
  description: 'Get current weather for a city',
  schema: z.object({
    city: z.string().describe('City name')
  }),
  // Widget config (registration-time metadata)
  widget: {
    name: 'weather-display',     // Must match widget in resources/
    invoking: 'Fetching weather...',
    invoked: 'Weather data loaded'
  }
}, async ({ city }) => {
  // Fetch data from API
  const data = await fetchWeatherAPI(city);
  
  // Return widget with runtime data
  return widget({
    props: {
      city,
      temperature: data.temp,
      conditions: data.conditions,
      humidity: data.humidity
    },
    output: text(`Weather in ${city}: ${data.temp}°C`),
    message: `Current weather for ${city}`
  });
});

server.listen();

Key Points:

  • widget: { name, invoking, invoked }
    on tool definition
  • widget({ props, output })
    helper returns runtime data
  • props
    passed to widget,
    output
    shown to model
  • Widget must exist in
    resources/
    folder

Static Assets

Use the

public/
folder for images, fonts, etc:

my-app/
├── resources/
├── public/              # Static assets
│   ├── images/
│   │   ├── logo.svg
│   │   └── banner.png
│   └── fonts/
└── index.ts

Using assets in widgets:

import { Image } from 'mcp-use/react';

function MyWidget() {
  return (
    <div>
      {/* Paths relative to public/ folder */}
      <Image src="/images/logo.svg" alt="Logo" />
      <img src={window.__getFile?.('images/banner.png')} alt="Banner" />
    </div>
  );
}

Components

McpUseProvider

Unified provider combining all common setup:

import { McpUseProvider } from 'mcp-use/react';

function MyWidget() {
  return (
    <McpUseProvider 
      autoSize         // Auto-resize widget
      viewControls     // Add debug/fullscreen buttons
      debug            // Show debug info
    >
      <div>Widget content</div>
    </McpUseProvider>
  );
}

Image Component

Handles both data URLs and public paths:

import { Image } from 'mcp-use/react';

function MyWidget() {
  return (
    <div>
      <Image src="/images/photo.jpg" alt="Photo" />
      <Image src="data:image/png;base64,..." alt="Data URL" />
    </div>
  );
}

ErrorBoundary

Graceful error handling:

import { ErrorBoundary } from 'mcp-use/react';

function MyWidget() {
  return (
    <ErrorBoundary
      fallback={<div>Something went wrong</div>}
      onError={(error) => console.error(error)}
    >
      <MyComponent />
    </ErrorBoundary>
  );
}

Testing

Using the Inspector

  1. Start development server:

    yarn dev
    
  2. Open Inspector:

    • Navigate to
      http://localhost:3000/inspector
  3. Test widgets:

    • Click Tools tab
    • Find your widget tool
    • Enter test parameters
    • Execute to see widget render
  4. Debug interactions:

    • Use browser console
    • Check RPC logs
    • Test state persistence
    • Verify tool calls

Testing in ChatGPT

  1. Enable Developer Mode:

    • Settings → Connectors → Advanced → Developer mode
  2. Add your server:

    • Go to Connectors tab
    • Add remote MCP server URL
  3. Test in conversation:

    • Select Developer Mode from Plus menu
    • Choose your connector
    • Ask ChatGPT to use your tools

Prompting tips:

  • Be explicit: "Use the weather-app connector's get-weather tool..."
  • Disallow alternatives: "Do not use built-in tools, only use my connector"
  • Specify input: "Call get-weather with { city: 'Tokyo' }"

Best Practices

Schema Design

Use descriptive schemas:

// ✅ Good
const schema = z.object({
  city: z.string().describe('City name (e.g., Tokyo, Paris)'),
  temperature: z.number().min(-50).max(60).describe('Temp in Celsius'),
});

// ❌ Bad
const schema = z.object({
  city: z.string(),
  temp: z.number(),
});

Theme Support

Always support both themes:

const { theme } = useWidget();

const bgColor = theme === 'dark' ? 'bg-gray-900' : 'bg-white';
const textColor = theme === 'dark' ? 'text-white' : 'text-gray-900';

Loading States

Always check

isPending
first:

const { props, isPending } = useWidget<MyProps>();

if (isPending) {
  return <LoadingSpinner />;
}

// Now safe to access props.field
return <div>{props.field}</div>;

Widget Focus

Keep widgets focused:

// ✅ Good: Single purpose
export const widgetMetadata: WidgetMetadata = {
  description: 'Display weather for a city',
  props: z.object({ city: z.string() }),
};

// ❌ Bad: Too many responsibilities
export const widgetMetadata: WidgetMetadata = {
  description: 'Weather, forecast, map, news, and more',
  props: z.object({ /* many fields */ }),
};

Error Handling

Handle errors gracefully:

const { callTool } = useWidget();

const fetchData = async () => {
  try {
    const result = await callTool('fetch-data', { id: '123' });
    if (result.isError) {
      console.error('Tool returned error');
    }
  } catch (error) {
    console.error('Tool call failed:', error);
  }
};

Configuration

Production Setup

Set base URL for production:

const server = new MCPServer({
  name: 'my-app',
  version: '1.0.0',
  baseUrl: process.env.MCP_URL || 'https://myserver.com'
});

Environment Variables

# Server URL
MCP_URL=https://myserver.com

# For static deployments
MCP_SERVER_URL=https://myserver.com/api
CSP_URLS=https://cdn.example.com,https://api.example.com

Variable usage:

  • MCP_URL
    : Base URL for widget assets and CSP
  • MCP_SERVER_URL
    : MCP server URL for tool calls (static deployments)
  • CSP_URLS
    : Additional domains for Content Security Policy

Deployment

Deploy to mcp-use Cloud

# Login
npx mcp-use login

# Deploy
yarn deploy

Build for Production

# Build
yarn build

# Start
yarn start

Build process:

  • Compiles TypeScript
  • Bundles React widgets
  • Optimizes assets
  • Generates production HTML

Common Patterns

Data Fetching Widget

const DataWidget: React.FC = () => {
  const { props, isPending, callTool } = useWidget();
  
  if (isPending) {
    return <div>Loading...</div>;
  }
  
  const refresh = async () => {
    await callTool('fetch-data', { id: props.id });
  };
  
  return (
    <div>
      <h1>{props.title}</h1>
      <button onClick={refresh}>Refresh</button>
    </div>
  );
};

Stateful Widget

const CounterWidget: React.FC = () => {
  const { state, setState } = useWidget();
  
  const increment = async () => {
    await setState({ 
      count: (state?.count || 0) + 1 
    });
  };
  
  return (
    <div>
      <p>Count: {state?.count || 0}</p>
      <button onClick={increment}>+1</button>
    </div>
  );
};

Themed Widget

const ThemedWidget: React.FC = () => {
  const { theme } = useWidget();
  
  return (
    <div className={theme === 'dark' ? 'dark-theme' : 'light-theme'}>
      Content
    </div>
  );
};

Troubleshooting

Widget Not Appearing

Problem: Widget file exists but tool doesn't appear

Solutions:

  • Ensure
    .tsx
    extension
  • Export
    widgetMetadata
    object
  • Export default React component
  • Check server logs for errors
  • Verify widget name matches file/folder name

Props Not Received

Problem: Component receives empty props

Solutions:

  • Check
    isPending
    first (props empty while pending)
  • Use
    useWidget()
    hook (not React props)
  • Verify
    widgetMetadata.props
    is valid Zod schema
  • Check tool parameters match schema

CSP Errors

Problem: Widget loads but assets fail

Solutions:

  • Set
    baseUrl
    in server config
  • Add domains to CSP via
    appsSdkMetadata
  • Use HTTPS for all resources
  • Check browser console for CSP violations

Learn More

Quick Reference

Commands:

  • npx create-mcp-use-app my-app --template apps-sdk
    - Bootstrap
  • yarn dev
    - Development with hot reload
  • yarn build
    - Build for production
  • yarn start
    - Run production server
  • yarn deploy
    - Deploy to mcp-use Cloud

Widget structure:

  • resources/widget-name.tsx
    - Single file widget
  • resources/widget-name/widget.tsx
    - Folder-based widget entry
  • public/
    - Static assets

Widget metadata:

  • description
    - Widget description
  • props
    - Zod schema for input
  • exposeAsTool
    - Auto-register as tool (default: true)
  • appsSdkMetadata
    - Apps SDK configuration

useWidget hook:

  • props
    - Widget input parameters
  • isPending
    - Loading state flag
  • state, setState
    - Persistent state
  • callTool
    - Call other tools
  • theme
    - Current theme (light/dark)
  • displayMode, requestDisplayMode
    - Display control