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
- Node 18+ (or Python 3.10+ if you prefer Python—patterns are the same)
- An HTTPS-reachable endpoint (Vercel/Render/Fly/Cloud Run all fine)
- Basic familiarity with REST and JSON
- Read these for context: Model Context Protocol (MCP) • MCP Server Tutorial • Inline UI & Widgets
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: Security • Data Privacy • Secrets Handling • Compliance & PII
7) Add analytics (minimum viable)
Track:
- Route hits (
/startrenders) - 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
ngrokif needed) - Fuzz invalid inputs to test validation + error UI
Deploy
- Push to your host of choice; set
MCP_BASEenv 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.
