Home / Blog / How to Create a ChatGPT App

How to Create a ChatGPT App: Complete Developer Guide (2025)

Building a custom ChatGPT app allows you to extend ChatGPT's capabilities with your own tools, data sources, and interactive UI components. In this comprehensive guide, you'll learn how to create a ChatGPT app from scratch using the OpenAI Apps SDK and the Model Context Protocol (MCP).

By the end of this tutorial, you'll have a working ChatGPT app with a backend MCP server, custom tools, and an interactive widget that renders directly inside the ChatGPT interface.

What Is a ChatGPT App?

A ChatGPT app (formerly called "connector") is an extension that lets ChatGPT interact with external services, data sources, and custom interfaces. Apps can:

The Apps SDK is built on the Model Context Protocol (MCP), an open specification for connecting large language models to external tools and resources.

Prerequisites and Requirements

Before you start building, make sure you have:

๐Ÿ“‹ Developer Mode Access

To test custom apps, you need to enable Developer Mode in ChatGPT. Go to Settings โ†’ Apps & Connectors โ†’ Advanced settings and toggle on Developer Mode. For Business/Enterprise plans, workspace admins must enable this first.

Understanding the Model Context Protocol (MCP)

The Model Context Protocol is the backbone of ChatGPT apps. It standardizes how AI models communicate with external services. An MCP server implements three core capabilities:

1. Tools

Tools are functions that ChatGPT can call during a conversation. Each tool has a name, description, and JSON Schema defining its input and output parameters. For example, a task management app might expose tools like create_task, list_tasks, and complete_task.

2. Resources

Resources are data sources that provide contextual information. In the Apps SDK, UI templates are exposed as resources with the special MIME type text/html+skybridge, which tells ChatGPT to render them as interactive widgets.

3. Transports

MCP supports multiple transport protocols. For ChatGPT apps, you'll use Streamable HTTP (recommended) or Server-Sent Events (SSE). The transport handles the communication layer between ChatGPT and your server.

Setting Up Your Project

Let's create a simple to-do list app. Start by setting up your project structure:

your-chatgpt-app/
โ”œโ”€โ”€ server/
โ”‚   โ””โ”€โ”€ src/
โ”‚       โ””โ”€โ”€ index.ts      # MCP server + tool handlers
โ”œโ”€โ”€ web/
โ”‚   โ””โ”€โ”€ public/
โ”‚       โ””โ”€โ”€ widget.html   # UI widget
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ tsconfig.json

Initialize the project

Create a new directory and initialize npm:

mkdir my-chatgpt-app
cd my-chatgpt-app
npm init -y

Install dependencies

Install the MCP SDK and required packages:

# For TypeScript/Node.js
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

# For Python
pip install mcp fastapi uvicorn

The @modelcontextprotocol/sdk package provides tool/schema helpers, HTTP server scaffolding, and end-to-end type safety so you can focus on business logic.

Building Your MCP Server

The MCP server is the core of your app. It defines tools, handles requests, and returns data to ChatGPT. Here's a complete example in TypeScript:

// server/src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import http from "http";

// In-memory task storage
const tasks: Array<{ id: string; title: string; done: boolean }> = [];

function createTodoServer() {
  const server = new McpServer({
    name: "todo-app",
    version: "1.0.0",
  });

  // Register the UI widget as a resource
  server.resource(
    "todo-widget",
    "todo://widget",
    async () => ({
      contents: [{
        uri: "todo://widget",
        mimeType: "text/html+skybridge",
        text: TODO_WIDGET_HTML, // Your HTML widget code
      }],
    })
  );

  // Define the "add_todo" tool
  server.tool(
    "add_todo",
    "Add a new task to the todo list",
    { title: z.string().describe("The task title") },
    async ({ title }) => {
      const task = { 
        id: crypto.randomUUID(), 
        title, 
        done: false 
      };
      tasks.push(task);
      
      return {
        content: [{ type: "text", text: `Added: ${title}` }],
        structuredContent: { tasks },
        _meta: {
          "openai/outputTemplate": "todo://widget"
        }
      };
    }
  );

  // Define the "complete_todo" tool
  server.tool(
    "complete_todo",
    "Mark a task as complete",
    { id: z.string().describe("The task ID") },
    async ({ id }) => {
      const task = tasks.find(t => t.id === id);
      if (task) task.done = true;
      
      return {
        content: [{ type: "text", text: `Completed task` }],
        structuredContent: { tasks },
        _meta: {
          "openai/outputTemplate": "todo://widget"
        }
      };
    }
  );

  return server;
}

// Start the HTTP server
const PORT = 8787;
const MCP_PATH = "/mcp";

const httpServer = http.createServer(async (req, res) => {
  const url = new URL(req.url || "/", `http://localhost`);
  
  // Handle CORS preflight
  if (req.method === "OPTIONS") {
    res.writeHead(204, {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "POST, GET, OPTIONS",
      "Access-Control-Allow-Headers": "content-type, mcp-session-id",
    });
    res.end();
    return;
  }

  // Handle MCP requests
  if (url.pathname === MCP_PATH) {
    res.setHeader("Access-Control-Allow-Origin", "*");
    
    const server = createTodoServer();
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
      enableJsonResponse: true,
    });

    await server.connect(transport);
    await transport.handleRequest(req, res);
  }
});

httpServer.listen(PORT, () => {
  console.log(`MCP server running at http://localhost:${PORT}${MCP_PATH}`);
});

Key concepts in this code

Creating the UI Widget

UI widgets render inside ChatGPT as iframes. They receive data through window.openai.toolOutput and can call tools via window.openai.callTool().

Create a file web/public/widget.html:

<!DOCTYPE html>
<html>
<head>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
      padding: 16px;
      margin: 0;
    }
    .task {
      display: flex;
      align-items: center;
      padding: 12px;
      border: 1px solid #e5e5e5;
      border-radius: 8px;
      margin-bottom: 8px;
    }
    .task.done {
      opacity: 0.5;
      text-decoration: line-through;
    }
    .task input[type="checkbox"] {
      margin-right: 12px;
    }
    .add-form {
      display: flex;
      gap: 8px;
      margin-bottom: 16px;
    }
    .add-form input {
      flex: 1;
      padding: 8px 12px;
      border: 1px solid #e5e5e5;
      border-radius: 6px;
    }
    .add-form button {
      padding: 8px 16px;
      background: #000;
      color: #fff;
      border: none;
      border-radius: 6px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div id="app"></div>
  
  <script>
    // Get data from ChatGPT
    const data = window.openai?.toolOutput || { tasks: [] };
    
    function render() {
      const app = document.getElementById('app');
      const tasks = data.tasks || [];
      
      app.innerHTML = \`
        <form class="add-form" onsubmit="addTask(event)">
          <input type="text" id="newTask" placeholder="Add a task...">
          <button type="submit">Add</button>
        </form>
        <div id="tasks">
          \${tasks.map(task => \`
            <div class="task \${task.done ? 'done' : ''}">
              <input 
                type="checkbox" 
                \${task.done ? 'checked' : ''} 
                onchange="toggleTask('\${task.id}')"
              >
              <span>\${task.title}</span>
            </div>
          \`).join('')}
        </div>
      \`;
    }
    
    async function addTask(e) {
      e.preventDefault();
      const input = document.getElementById('newTask');
      const title = input.value.trim();
      if (!title) return;
      
      // Call the MCP tool
      const result = await window.openai.callTool('add_todo', { title });
      data.tasks = result.tasks;
      input.value = '';
      render();
    }
    
    async function toggleTask(id) {
      const result = await window.openai.callTool('complete_todo', { id });
      data.tasks = result.tasks;
      render();
    }
    
    render();
  </script>
</body>
</html>

The window.openai API

ChatGPT injects the window.openai object into your widget with these properties:

Connecting to ChatGPT

With your server running, you need to expose it to the internet and register it in ChatGPT.

Step 1: Start your server

npm run build
node dist/index.js
# Server running at http://localhost:8787/mcp

Step 2: Create a tunnel with ngrok

ChatGPT requires HTTPS, so use ngrok to create a secure tunnel:

ngrok http 8787
# Forwarding: https://abc123.ngrok.app -> http://localhost:8787

Step 3: Add the connector in ChatGPT

  1. Open ChatGPT Settings โ†’ Connectors
  2. Click Create (requires Developer Mode enabled)
  3. Enter your connector details:
    • Name: Todo App
    • Description: Manage your tasks
    • URL: https://abc123.ngrok.app/mcp
  4. Click Create

If successful, you'll see a list of tools your server exposes.

Step 4: Test your app

  1. Start a new chat in ChatGPT
  2. Click the + button and select your connector from "More"
  3. Ask ChatGPT: "Add a task to read a book"
  4. Watch your widget render with the new task!

Deploying to Production

For production, deploy your MCP server to a reliable HTTPS host. Popular options include:

Production checklist

Best Practices and Tips

Tool Design

UI Widgets

Security

Skip the complexity

Building MCP servers requires backend development, hosting, and ongoing maintenance. Agentappbuilder lets you create the same apps visually โ€” no code required.

Join the waitlist

Conclusion

You've learned how to create a ChatGPT app from scratch using the OpenAI Apps SDK and Model Context Protocol. The key steps are:

  1. Set up an MCP server with tools and resources
  2. Create a UI widget that reads window.openai.toolOutput
  3. Expose your server via HTTPS
  4. Register it in ChatGPT's connector settings
  5. Deploy to production with proper security

While building from scratch gives you full control, it requires significant development effort. For teams without backend expertise or those who want to ship faster, no-code platforms like Agentappbuilder can dramatically reduce time to market.

Further resources