chatgpt-app:new

Create a new ChatGPT App from concept to working code. Guides through conceptualization, design, implementation, testing, and deployment.

16 stars

Best use case

chatgpt-app:new is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Create a new ChatGPT App from concept to working code. Guides through conceptualization, design, implementation, testing, and deployment.

Teams using chatgpt-app:new should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/chatgpt-app-new/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/ai-agents/chatgpt-app-new/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/chatgpt-app-new/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How chatgpt-app:new Compares

Feature / Agentchatgpt-app:newStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Create a new ChatGPT App from concept to working code. Guides through conceptualization, design, implementation, testing, and deployment.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Create a New ChatGPT App

You are helping the user create a new ChatGPT App. Follow this multi-phase workflow to take them from concept to a working, deployable application.

## CRITICAL REQUIREMENTS

**ChatGPT Apps MUST use:**
- `Server` class from `@modelcontextprotocol/sdk/server/index.js` (NOT `McpServer`)
- `StreamableHTTPServerTransport` from `@modelcontextprotocol/sdk/server/streamableHttp.js`
- Session management with `Map<string, StreamableHTTPServerTransport>`
- Widget URIs: `ui://widget/{widget-id}.html`
- Widget MIME type: `text/html+skybridge`
- `structuredContent` in tool responses for widget data
- `_meta` with `openai/outputTemplate` on both tool definitions and responses

## File Structure

Every ChatGPT App follows this structure:

```
{app-name}/
├── package.json              # Dependencies and scripts
├── tsconfig.server.json      # TypeScript config
├── setup.sh                  # One-command setup
├── START.sh                  # Multi-mode server launcher
├── .env                      # Environment variables (created by setup.sh)
├── .env.example              # Environment template
├── .gitignore                # Git ignores
└── server/
    └── index.ts              # Complete MCP server with inline widgets
```

## Phase 1: Conceptualization

Start by gathering information about the app:

1. **Ask for the app idea**
   "What ChatGPT App would you like to build? Describe what it does and the problem it solves."

2. **Analyze against UX Principles**
   Evaluate the idea against the three pillars:
   - **Conversational Leverage**: What can users accomplish through natural language that would be harder in a traditional UI?
   - **Native Fit**: How does this integrate naturally with ChatGPT's conversational flow?
   - **Composability**: Can the tools work independently and combine with other apps?

3. **Check for Anti-Patterns**
   Warn if the idea includes:
   - Static website content display
   - Complex multi-step workflows requiring external tabs
   - Duplicating ChatGPT's native capabilities
   - Ads or upsells

4. **Define Use Cases**
   Create 3-5 primary use cases with user stories.

## Phase 2: Design

Design the technical architecture:

1. **Tool Topology**
   Define the MCP tools needed:
   - Query tools (readOnlyHint: true)
   - Mutation tools (destructiveHint: false)
   - Destructive tools (destructiveHint: true)
   - Widget tools (return UI with _meta)
   - External API tools (openWorldHint: true)

2. **Widget Design**
   For each widget, define:
   - `id` - unique identifier (kebab-case)
   - `name` - display name
   - `description` - what it shows
   - `mockData` - sample data for preview

3. **Data Model**
   Design entities and their relationships.

4. **Auth Requirements**
   - Single-user (no auth needed)
   - Multi-user (Auth0 or Supabase Auth)

## Phase 3: Implementation

Generate the complete application code.

### package.json

```json
{
  "name": "{app-name}",
  "version": "1.0.0",
  "description": "{app-description}",
  "type": "module",
  "scripts": {
    "build": "npm run build:server",
    "build:server": "tsc -p tsconfig.server.json",
    "start": "HTTP_MODE=true node dist/server/index.js",
    "dev": "HTTP_MODE=true NODE_ENV=development tsx watch --clear-screen=false server/index.ts",
    "validate": "tsc --noEmit -p tsconfig.server.json"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "dotenv": "^16.4.0",
    "express": "^4.18.2",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.4.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}
```

### tsconfig.server.json

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist/server",
    "rootDir": "server",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "sourceMap": true
  },
  "include": ["server/**/*"],
  "exclude": ["node_modules", "dist"]
}
```

### server/index.ts Structure

The server MUST follow this structure:

```typescript
// 1. Load environment first
import "dotenv/config";
import express, { Request, Response } from "express";
import { randomUUID } from "crypto";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  JSONRPCMessage,
} from "@modelcontextprotocol/sdk/types.js";

// 2. Configuration
const PORT = process.env.PORT || 3000;
const NODE_ENV = process.env.NODE_ENV || "development";
const WIDGET_DOMAIN = process.env.WIDGET_DOMAIN || `http://localhost:${PORT}`;

function log(...args: unknown[]) {
  if (NODE_ENV === "development") {
    console.log(`[${new Date().toISOString()}]`, ...args);
  }
}

// 3. Widget Configuration Array
interface WidgetConfig {
  id: string;
  name: string;
  description: string;
  templateUri: string;
  invoking: string;
  invoked: string;
  mockData: Record<string, unknown>;
}

const widgets: WidgetConfig[] = [
  {
    id: "my-widget",
    name: "My Widget",
    description: "Displays data in a visual format",
    templateUri: "ui://widget/my-widget.html",
    invoking: "Loading...",
    invoked: "Ready",
    mockData: { /* sample data */ },
  },
];

const WIDGETS_BY_ID = new Map(widgets.map((w) => [w.id, w]));
const WIDGETS_BY_URI = new Map(widgets.map((w) => [w.templateUri, w]));

// 4. Inline Widget HTML Generator
function generateWidgetHtml(widgetId: string, previewData?: Record<string, unknown>): string {
  const widget = WIDGETS_BY_ID.get(widgetId);
  if (!widget) return `<html><body>Widget not found: ${widgetId}</body></html>`;

  const previewScript = previewData
    ? `<script>window.PREVIEW_DATA = ${JSON.stringify(previewData)};</script>`
    : "";

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${widget.name}</title>
  <style>/* CSS styles */</style>
  ${previewScript}
</head>
<body>
  <div id="root"><div class="loading">Loading...</div></div>
  <script>
    (function() {
      let rendered = false;

      function render(data) {
        if (rendered || !data) return;
        rendered = true;
        // Widget rendering logic
        document.getElementById('root').innerHTML = '...';
      }

      function tryRender() {
        if (window.PREVIEW_DATA) { render(window.PREVIEW_DATA); return; }
        if (window.openai?.toolOutput) { render(window.openai.toolOutput); }
      }

      // ChatGPT Apps SDK integration
      window.addEventListener('openai:set_globals', tryRender);

      // Polling fallback
      const poll = setInterval(() => {
        if (window.openai?.toolOutput || window.PREVIEW_DATA) {
          tryRender();
          clearInterval(poll);
        }
      }, 100);
      setTimeout(() => clearInterval(poll), 10000);

      tryRender();
    })();
  </script>
</body>
</html>`;
}

// 5. Tool Definitions
const tools = [
  {
    name: "my_tool",
    description: "Does something useful",
    inputSchema: {
      type: "object" as const,
      properties: { /* ... */ },
      required: ["..."],
    },
    annotations: { title: "My Tool", readOnlyHint: true, destructiveHint: false, openWorldHint: false },
    widgetId: "my-widget", // Links to widget config (optional)
    execute: (args: any) => {
      // Tool logic - return data that widget will display
      return { /* result data */ };
    },
  },
];

// 6. MCP Server Factory
function createServer(): Server {
  const server = new Server(
    { name: "{app-name}", version: "1.0.0" },
    { capabilities: { tools: {}, resources: {} } }
  );

  // ListTools handler
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    log("ListTools request");
    return {
      tools: tools.map((tool) => {
        const widget = tool.widgetId ? WIDGETS_BY_ID.get(tool.widgetId) : null;
        return {
          name: tool.name,
          description: tool.description,
          inputSchema: tool.inputSchema,
          annotations: tool.annotations,
          ...(widget && {
            _meta: {
              "openai/outputTemplate": widget.templateUri,
              "openai/widgetAccessible": true,
              "openai/resultCanProduceWidget": true,
              "openai/toolInvocation/invoking": widget.invoking,
            },
          }),
        };
      }),
    };
  });

  // CallTool handler
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;
    log(`CallTool: ${name}`, args);

    const tool = tools.find((t) => t.name === name);
    if (!tool) throw new Error(`Unknown tool: ${name}`);

    try {
      const result = tool.execute(args);
      const widget = tool.widgetId ? WIDGETS_BY_ID.get(tool.widgetId) : null;

      if (widget) {
        return {
          content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
          structuredContent: result,  // CRITICAL: This becomes window.openai.toolOutput
          _meta: {
            "openai/outputTemplate": widget.templateUri,
            "openai/toolInvocation/invoked": widget.invoked,
          },
        };
      }

      return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] };
    } catch (error) {
      return {
        content: [{ type: "text" as const, text: `Error: ${error instanceof Error ? error.message : "Unknown"}` }],
        isError: true,
      };
    }
  });

  // ListResources handler
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
    return {
      resources: widgets.map((w) => ({
        uri: w.templateUri,
        name: w.name,
        description: w.description,
        mimeType: "text/html+skybridge",
      })),
    };
  });

  // ReadResource handler
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
    const { uri } = request.params;
    const widget = WIDGETS_BY_URI.get(uri);
    if (!widget) throw new Error(`Unknown resource: ${uri}`);

    return {
      contents: [{ uri, mimeType: "text/html+skybridge", text: generateWidgetHtml(widget.id) }],
      _meta: {
        "openai/serialization": "markdown-encoded-html",
        "openai/csp": { script_domains: ["'unsafe-inline'"], connect_domains: [WIDGET_DOMAIN] },
      },
    };
  });

  return server;
}

// 7. Express App with Session Management
const app = express();
app.use(express.json());

const transports = new Map<string, StreamableHTTPServerTransport>();

// Health endpoint
app.get("/health", (req, res) => {
  res.json({ status: "ok", service: "{app-name}", widgets: widgets.length });
});

// Widget preview index
app.get("/preview", (req, res) => {
  res.send(`<!DOCTYPE html>
    <html><head><title>Widget Preview</title></head>
    <body>
      <h1>Widget Preview</h1>
      ${widgets.map(w => `<a href="/preview/${w.id}">${w.name}</a><br>`).join('')}
    </body></html>`);
});

// Widget preview with mock data
app.get("/preview/:widgetId", (req, res) => {
  const widget = WIDGETS_BY_ID.get(req.params.widgetId);
  if (!widget) { res.status(404).send("Widget not found"); return; }
  res.setHeader("Content-Type", "text/html");
  res.send(generateWidgetHtml(widget.id, widget.mockData));
});

// MCP endpoint with session management
app.all("/mcp", async (req, res) => {
  log("MCP request:", req.method, req.headers["mcp-session-id"] || "no-session");

  let sessionId = req.headers["mcp-session-id"] as string | undefined;
  let transport = sessionId ? transports.get(sessionId) : undefined;

  const isInitialize = req.body?.method === "initialize" ||
    (Array.isArray(req.body) && req.body.some((m: JSONRPCMessage) => "method" in m && m.method === "initialize"));

  if (isInitialize || !sessionId || !transport) {
    sessionId = randomUUID();
    log(`New session: ${sessionId}`);

    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => sessionId!,
      onsessioninitialized: (id) => log(`Session initialized: ${id}`),
    });

    transports.set(sessionId, transport);
    const server = createServer();

    res.on("close", () => log(`Connection closed: ${sessionId}`));
    transport.onclose = () => { transports.delete(sessionId!); server.close(); };

    await server.connect(transport);
  }

  await transport.handleRequest(req, res, req.body);
});

app.delete("/mcp", async (req, res) => {
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  if (sessionId && transports.has(sessionId)) {
    await transports.get(sessionId)!.handleRequest(req, res, req.body);
  } else {
    res.status(404).json({ error: "Session not found" });
  }
});

// 8. Start Server
app.listen(PORT, () => {
  console.log(`{App Name} MCP Server running on port ${PORT}`);
  console.log(`  MCP:     http://localhost:${PORT}/mcp`);
  console.log(`  Health:  http://localhost:${PORT}/health`);
  console.log(`  Preview: http://localhost:${PORT}/preview`);
});
```

### setup.sh

```bash
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"

echo "=== {App Name} Setup ==="

# Check Node.js 18+
NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
if [ "$NODE_VERSION" -lt 18 ]; then
  echo "Node.js 18+ required"; exit 1
fi

npm install
npm run build:server

if [ ! -f .env ]; then
  cat > .env << 'EOF'
PORT=3000
HTTP_MODE=true
NODE_ENV=development
WIDGET_DOMAIN=http://localhost:3000
EOF
fi

chmod +x START.sh
echo "Setup complete! Run ./START.sh --dev"
```

### START.sh

```bash
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"

if [ -f .env ]; then set -a; source .env; set +a; fi

case "${1:-}" in
  --dev)
    echo "Dev mode: http://localhost:${PORT:-3000}/preview"
    npm run dev
    ;;
  --preview)
    npm run dev &
    sleep 2
    open "http://localhost:${PORT:-3000}/preview"
    wait
    ;;
  --stdio)
    HTTP_MODE=false node dist/server/index.js
    ;;
  *)
    [ ! -f dist/server/index.js ] && npm run build:server
    HTTP_MODE=true node dist/server/index.js
    ;;
esac
```

## Phase 4: Testing

Before deployment:

1. **Run setup**: `./setup.sh`
2. **Start dev mode**: `./START.sh --dev`
3. **Preview widgets**: Open `http://localhost:3000/preview`
4. **Test health**: `curl http://localhost:3000/health`
5. **Test MCP**: Connect via MCP Inspector or ChatGPT

## Phase 5: Deployment

Deploy to Render:

1. Create `Dockerfile`:
```dockerfile
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist
EXPOSE 3000
CMD ["node", "dist/server/index.js"]
```

2. Create `render.yaml`:
```yaml
services:
  - type: web
    name: {app-name}
    runtime: docker
    plan: free
    healthCheckPath: /health
    envVars:
      - key: PORT
        value: 3000
      - key: HTTP_MODE
        value: true
      - key: NODE_ENV
        value: production
```

3. Push to GitHub and connect to Render
4. ChatGPT connector URL: `https://{app-name}.onrender.com/mcp`

## State Persistence

Save progress to `.chatgpt-app/state.json` after each phase.

## Getting Started

When invoked, begin with:
"What ChatGPT App would you like to build? Describe what it does and the problem it solves."

Then guide them through each phase systematically.

Related Skills

chatgpt

16
from diegosouzapw/awesome-omni-skill

OpenAI's conversational AI assistant.

chatgpt-import

16
from diegosouzapw/awesome-omni-skill

Import ChatGPT conversation history into OpenClaw's memory search. Use when migrating from ChatGPT, giving OpenClaw access to old conversations, or building a searchable archive of past chats.

chatgpt-exporter-ultimate

16
from diegosouzapw/awesome-omni-skill

Export ALL your ChatGPT conversations instantly — no 24h wait, no extensions. Works via browser relay OR standalone bookmarklet. Extracts full message history with timestamps, roles, and metadata. One command, one JSON file, done.

boycott-chatgpt-54c8dfea

16
from diegosouzapw/awesome-omni-skill

OpenAI president Greg Brockman gave [$25 million](https://www.sfgate.com/tech/article/brockman-openai-top-trump-donor-21273419.php) to MAGA Inc in 2025. They gave Trump 26x more than any other major AI company. ICE's resume screening tool is powered by OpenAI's GPT-4. They're spending 50 million dollars to prevent states from regulating AI.

how-to-build-chatgpt-sidebar

16
from diegosouzapw/awesome-omni-skill

Use when asked to build a sidebar experience similar to ChatGPT.com / OpenAI

guard-users-chatgpt

16
from diegosouzapw/awesome-omni-skill

Guardrail policy for Chatgpt CLI: refuse catastrophic actions, require scoped approvals, and reduce secret leakage.

chatgpt / 启用开发者模式的 / openai

16
from diegosouzapw/awesome-omni-skill

General SOP for common requests related to chatgpt, 启用开发者模式的, openai.

chatgpt-history

16
from diegosouzapw/awesome-omni-skill

Search and extract data from ChatGPT conversation history. Use when the user asks to find, search, or extract information from their ChatGPT history, conversations, or past chats.

chatgpt-apps

16
from diegosouzapw/awesome-omni-skill

Complete ChatGPT Apps builder - Create, design, implement, test, and deploy ChatGPT Apps with MCP servers, widgets, auth, database integration, and automated deployment

chatgpt-apps-sdk

16
from diegosouzapw/awesome-omni-skill

Build ChatGPT apps using OpenAI's Apps SDK. This skill leverages OpenAI's Docs MCP server to fetch the latest documentation, ensuring guidance is always current. Use when creating a new ChatGPT app, building an MCP server for ChatGPT, designing widgets/UI for ChatGPT apps, preparing an app for submission, or any question about ChatGPT Apps SDK or Agentic Commerce.

chatgpt-app

16
from diegosouzapw/awesome-omni-skill

Guidance for building the chatgpt-app (Vite + React + @openai/apps-sdk-ui) with the MCP-friendly single-file output.

chatgpt-app:validate

16
from diegosouzapw/awesome-omni-skill

Run validation suite on your ChatGPT App to check schemas, annotations, widgets, and UX compliance.