Skip to content
Back to skills

Chatgpt App Builder Neversight

ASecurity

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.

  • 57 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 17, 2026
developmenttypescriptgobashreacttestingdebugginggitapisecuritydocumentation

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 17, 2026

npx -y skills add diegosouzapw/awesome-omni-skill --skill chatgpt-app-builder-neversight --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Chatgpt App Builder Neversight?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Chatgpt App Builder Neversight
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/diegosouzapw-chatgpt-app-builder-neversight/badge)](https://www.skillsdirectory.com/skills/diegosouzapw-chatgpt-app-builder-neversight)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: chatgpt-app-builder
description: 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.
---

# 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:**

```bash
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`:

```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`):**

```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:

```typescript
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:

```tsx
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`:

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:

```typescript
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:**

```tsx
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:

```tsx
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:

```tsx
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:

```tsx
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:**
   ```bash
   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:

```typescript
// ✅ 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:

```tsx
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:

```tsx
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:

```typescript
// ✅ 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:

```tsx
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:

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

### Environment Variables

```env
# 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

```bash
# Login
npx mcp-use login

# Deploy
yarn deploy
```

### Build for Production

```bash
# Build
yarn build

# Start
yarn start
```

Build process:
- Compiles TypeScript
- Bundles React widgets
- Optimizes assets
- Generates production HTML

## Common Patterns

### Data Fetching Widget

```tsx
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

```tsx
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

```tsx
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

- **Documentation**: https://docs.mcp-use.com
- **Widget Guide**: https://docs.mcp-use.com/typescript/server/ui-widgets
- **Apps SDK Tutorial**: https://docs.mcp-use.com/typescript/server/creating-apps-sdk-server
- **ChatGPT Apps Flow**: https://docs.mcp-use.com/guides/chatgpt-apps-flow
- **Inspector Debugging**: https://docs.mcp-use.com/inspector/debugging-chatgpt-apps
- **GitHub**: https://github.com/mcp-use/mcp-use

## 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

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.