Apps SDK Tutorial: Build Your First ChatGPT App

Goal: ship a minimal, production-ready ChatGPT App that renders a simple inline form, calls an external API via an MCP server, and returns a preview card. You’ll also wire basic security and analytics so it’s submission-ready.

If you’re brand new, start with What Are ChatGPT Apps? and How ChatGPT Apps Work. For tradeoffs vs agents, see Apps vs Agents.


What we’ll build

A “Text → Summary” app:

  • Inline form (textarea + length dropdown)
  • MCP tool call to your server (summarize)
  • Preview card with bullet summary
  • Minimal scopes (none, since we don’t touch user data)
  • Submission-friendly UX (validation, clear errors)

Prerequisites


1) Project scaffold

mkdir chatgpt-app-summarizer && cd chatgpt-app-summarizer
npm init -y
npm i express zod pino

Structure

/chatgpt-app-summarizer
  ├─ app.json              # App manifest (name, description, capabilities)
  ├─ server.js             # MCP server: exposes tools
  ├─ mcp-schema.ts         # (optional) zod schemas for tool input/output
  ├─ ui-templates.json     # Inline UI components (form, preview)
  └─ README.md

2) Define your app manifest (listing basics)

app.json

{
  "name": "Summarizer",
  "short_description": "Turn long text into concise bullet summaries.",
  "capabilities": ["summarize"],
  "categories": ["productivity", "writing"],
  "example_prompts": [
    "Summarize this text in 5 bullets.",
    "Make a 1-paragraph executive summary."
  ],
  "permissions": [],
  "privacy": {
    "data_access": "Processes text you provide; does not store user data.",
    "revocation": "You can revoke access anytime in ChatGPT settings."
  }
}

Keep claims modest and precise—this copy influences in-chat suggestions and Directory matching. See App Directory and Ranking & SEO.


3) Build the MCP tool (server)

server.js

import express from "express";
import pino from "pino";
import { z } from "zod";

const log = pino();
const app = express();
app.use(express.json({ limit: "1mb" }));

// Zod schemas for tool I/O
const SummarizeInput = z.object({
  text: z.string().min(40, "Enter at least 40 characters."),
  length: z.enum(["short", "medium", "detailed"]).default("short")
});
const SummarizeOutput = z.object({
  bullets: z.array(z.string()).min(1)
});

// MCP tool endpoint
app.post("/tools/summarize", async (req, res) => {
  try {
    const input = SummarizeInput.parse(req.body?.input ?? {});
    const bullets = summarize(input.text, input.length);
    const output = SummarizeOutput.parse({ bullets });

    // MCP response shape (generic example)
    res.json({
      type: "tool_result",
      tool: "summarize",
      output
    });
  } catch (err) {
    log.error({ err }, "summarize failed");
    res.status(400).json({
      type: "tool_error",
      tool: "summarize",
      message: err?.message || "Invalid input"
    });
  }
});

function summarize(text, length) {
  // Simple heuristic; swap for your LLM or API call.
  const sentences = text.split(/[\.\!\?]\s+/).filter(Boolean);
  const take = length === "short" ? 3 : length === "medium" ? 5 : 8;
  return sentences.slice(0, take).map(s => "• " + s.trim());
}

app.get("/health", (_, res) => res.json({ ok: true }));

app.listen(process.env.PORT || 3000, () => {
  log.info("MCP server running");
});

Your endpoint returns an MCP-style tool result the Apps SDK can consume. For deeper server patterns (auth, retries), see MCP Server Tutorial.


4) Add inline UI (form → preview)

ui-templates.json

{
  "form": {
    "type": "form",
    "title": "Summarize Text",
    "fields": [
      { "id": "text", "type": "textarea", "label": "Paste text", "minLength": 40, "required": true },
      { "id": "length", "type": "select", "label": "Summary length", "options": [
        {"label":"Short (≈3 bullets)","value":"short"},
        {"label":"Medium (≈5 bullets)","value":"medium"},
        {"label":"Detailed (≈8 bullets)","value":"detailed"}
      ], "required": true, "default": "short" }
    ],
    "submitLabel": "Summarize"
  },
  "preview": {
    "type": "card",
    "title": "Summary",
    "itemsBinding": "output.bullets"  // binds to tool output
  },
  "errors": {
    "validation": "Please check your inputs and try again.",
    "tool": "The summarizer failed. Try again in a moment."
  }
}

The exact widget schema depends on the Apps SDK UI contracts—use this as a mental model. See Inline UI & Widgets for patterns that convert.


5) Wire the action (client handler)

In your Apps SDK configuration (conceptual):

import ui from "./ui-templates.json";

export default {
  name: "Summarizer",
  routes: [
    {
      path: "/start",
      render: ui.form,
      onSubmit: async ({ text, length }) => {
        const resp = await fetch(process.env.MCP_BASE + "/tools/summarize", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ input: { text, length } })
        });
        const json = await resp.json();
        if (json.type === "tool_error") throw new Error(json.message);
        return {
          render: {
            ...ui.preview,
            data: { output: json.output }
          }
        };
      }
    }
  ]
};

The Apps SDK handles the handshake with ChatGPT and renders your UI inline. Keep your first-run path under 60 seconds. See How ChatGPT Apps Work.


6) Add basic security

  • No extra scopes for this MVP (least privilege).
  • Rate-limit /tools/* endpoints; implement timeouts/retries.
  • Mask logs; never log raw PII.
  • Rotate API keys (if you call external services).
  • Prepare a deletion path if you add persistence.

Guides: SecurityData PrivacySecrets HandlingCompliance & PII


7) Add analytics (minimum viable)

Track:

  • Route hits (/start renders)
  • Submit success/failure
  • Time to first result
  • Error reasons (validation vs tool vs network)

See: Analytics for ChatGPT Apps


8) Test locally & deploy

Local

PORT=3000 node server.js
curl http://localhost:3000/health
  • Run form submission against localhost (tunnel with ngrok if needed)
  • Fuzz invalid inputs to test validation + error UI

Deploy

  • Push to your host of choice; set MCP_BASE env var to your HTTPS server URL
  • Smoke test the form, summary, and preview in ChatGPT

9) Prep your listing & submit

  • Title: “Summarizer: Turn long text into clear bullets
  • Short description: “Paste text → pick length → get concise bullets.
  • Media: 3 screenshots (form, loading, preview)
  • Example prompts:
    • “Summarize this earnings call in 5 bullets”
    • “Executive summary of the text below (1 paragraph)”
  • Category: Productivity → Writing
  • Changelog: “Initial release with 3 summary lengths.”

Then follow ChatGPT App Submission and App Verification & Review.


10) Next steps (polish & grow)

  • Add language detection and sentence segmentation per locale
  • Provide export to docs/email
  • Consider freemium with daily limits; enable in-chat checkout later via ACP
  • Expand with one adjacent use case (e.g., “key takeaways” or “action items”)

For more advanced builds, compare MCP vs Tools API and explore Agent Orchestration if you need multi-step execution.


FAQ

Do I need OAuth for this MVP?
Not if you only transform user-provided text. Add OAuth when you access third-party accounts (docs, calendars, storage).

How big can inputs be?
Keep payloads modest; chunk large text and stream partial results where possible.

Can I add an Agent later?
Yes—use the App for structured input and hand off execution to an Agent. Start with AgentKit Overview and AgentKit Tutorial.




Similar Posts