AI
Panduan ChatGPT Plugins (Apps SDK): Bangun Plugin ERP di MCP
Oktober 202611 menit baca

Ya, secara praktis. Apps SDK diluncurkan pada Oktober 2025 untuk membangun apps di ChatGPT, dan pada 9 Juli 2026 Plugin Directory menggantikan App Directory, dengan apps yang sudah ada dikemas menjadi plugins. Alamat developers.openai.com/apps-sdk kini redirect ke dokumentasi plugins, dan kode MCP server dari era Apps SDK tetap berlaku.
Sebuah plugin bisa berisi MCP server yang mengekspos tools, skills yang didefinisikan di file SKILL.md, dan UI opsional yang dirender ketika tool mengembalikan hasil. Hanya bagian yang Anda perlukan yang wajib, dan dokumentasi OpenAI mengizinkan plugin yang isinya skills saja. Manifest-nya berupa plugin.json di root, dengan pengaturan khusus OpenAI di bawah extensions.com.openai.
Tidak. Plugin beta tahun 2023 dimatikan pada April 2024. Plugins tahun 2026 memakai nama yang sama tetapi dibangun di atas Model Context Protocol, fondasi yang sama dengan Apps SDK, dan membundel apps, skills, serta app templates.
Daftarkan resource HTML dengan URI ui:// dan MIME type text/html;profile=mcp-app, lalu tautkan tool ke resource itu lewat _meta.ui.resourceUri. ChatGPT merender resource tersebut di frame yang di-sandbox dan mengirim hasil tool lewat postMessage. Package @modelcontextprotocol/ext-apps menyediakan registerAppTool, registerAppResource, dan class App untuk view-nya.
Anda butuh verifikasi organisasi serta hak owner atau role Apps Management Write. MCP app yang direview publik juga butuh URL website, support, privacy policy, dan terms, akun test khusus, lima test case positif dan tiga negatif, video walkthrough, serta ikon. Setelah disetujui, Anda sendiri yang menentukan kapan memilih Publish plugin.

Ringkasan Utama
ChatGPT plugin di 2026 adalah Apps SDK yang berganti nama dan diperluas: remote MCP server untuk tools, skill SKILL.md opsional untuk workflow, dan UI MCP Apps opsional yang dikembalikan tool lewat resource ui://. Bangun server-nya dulu, uji di developer mode, lalu submit lewat platform dashboard setelah verifikasi organisasi.
Saya kembali membuka dokumentasi Apps SDK untuk membangun integrasi ChatGPT kecil bagi sebuah ERP, yaitu tool yang menjawab posisi sebuah sales order, dan URL-nya justru mendarat di halaman berjudul Plugins. Tutorial yang saya bookmark menyebut apps, pengaturan ChatGPT menyebut plugins, dan jawaban lama masih menyebut connectors. Ketiganya lini produk yang sama dengan tiga nama, dan kode di bawahnya nyaris tidak berubah.
Tulisan ini mengurai nama-nama itu lengkap dengan tanggalnya, lalu membangun produknya: MCP server read-only dengan satu tool ERP, kartu status order yang dirender sebagai MCP App, skill yang membungkus tool itu dalam workflow follow-up, pengujian lokal, dan apa saja yang diminta saat submission ke directory. Setiap nama API berasal dari dokumentasi plugin OpenAI atau repository resmi ext-apps, dengan sumber di akhir tulisan.
Kalau Anda mencari ChatGPT plugins Apps SDK dan menemukan halaman yang saling bertentangan, itu karena penamaannya berpindah tiga kali dalam kurang dari setahun. Tidak satu pun perpindahan itu merusak kontrak dasarnya: ChatGPT terhubung ke remote MCP server dan memanggil tools-nya.
| Kapan | Namanya | Yang berubah bagi developer |
|---|---|---|
| 6 Oktober 2025 | Apps di ChatGPT, dibangun dengan Apps SDK (preview) | SDK dibangun di atas MCP dan memperluasnya sehingga satu server mendefinisikan logic sekaligus interface |
| 2025 | Connectors, lalu dilebur ke dalam apps | Integrasi sumber data seperti Google Drive dan SharePoint tidak lagi menjadi konsep terpisah |
| 9 Juli 2026 | Plugins, tercantum di Plugin Directory | Apps yang sudah ada dikemas menjadi plugins, yang bisa membundel apps, skills, dan app templates |
| Sekarang | developers.openai.com/apps-sdk redirect ke /plugins | Dokumentasinya menjelaskan cara membangun plugins dengan skills, MCP server, dan UI opsional |
Konsekuensi praktisnya: tutorial Apps SDK dari akhir 2025 sebagian besar masih benar soal MCP server, tetapi sebagian besar keliru soal packaging dan distribusi. Pertahankan kode server-nya, baca ulang semua hal tentang tempat publikasinya. Perlu dicatat juga bahwa plugins ini tidak berhubungan dengan plugin beta 2023, yang dimatikan pada April 2024. Kata yang sama, sistem yang berbeda.
Halaman arsitektur plugin OpenAI mendaftar bagian-bagiannya, dan hanya sebagian yang wajib. Mulailah dari bentuk terkecil yang mencakup use case Anda; dokumentasinya secara eksplisit mengizinkan MCP server atau UI ditambahkan belakangan.
erp-order-status/
├── plugin.json # manifest: name, version, description, OpenAI extensions
├── mcp.json # where the remote MCP server lives
├── .app.json # maps the registered MCP app; referenced from plugin.json
└── skills/
└── late-order-follow-up/
└── SKILL.md # the workflow the model follows around the tool
# plugin.json
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "erp-order-status",
"version": "1.0.0",
"description": "Look up sales order status from the ERP and draft customer updates.",
"extensions": {
"com.openai": {
"apps": "./.app.json",
"interface": { "displayName": "ERP Order Status", "category": "Productivity" }
}
}
}
# mcp.json
{
"$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
"mcpServers": {
"erp-order-status": {
"type": "streamable-http",
"url": "https://erp-mcp.example.com/mcp"
}
}
}Manifest-nya berupa plugin.json portabel di root, dengan field khusus OpenAI di bawah blok extensions.com.openai. Pemisahan ini penting kalau Anda juga menargetkan Codex: menurut OpenAI, satu plugin yang dipublikasikan masuk ke directory yang dipakai bersama oleh ChatGPT dan Codex, dan Codex membaca .codex-plugin/plugin.json sebagai fallback.
Tool ini hanya menjawab satu pertanyaan: berdasarkan nomor sales order, kembalikan stage, tanggal janji kirim, dan jumlah terkirim per baris. Datanya dibaca dari read model di atas ERP, tidak pernah dari endpoint yang bisa memposting dokumen. Registrasi di bawah mengikuti bentuk contoh resmi ext-apps, yang memakai registerAppTool supaya tool bisa membawa tautan ke UI-nya.
// server.ts
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE, // "text/html;profile=mcp-app"
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import { z } from "zod";
import { findSalesOrder } from "./erp.js"; // read model over the ERP, never the write API
const ORDER_CARD_URI = "ui://order-status/v1.html";
export function createServer(): McpServer {
const server = new McpServer({ name: "erp-order-status", version: "1.0.0" });
registerAppTool(
server,
"get_order_status",
{
title: "Get sales order status",
// The description is routing logic: it decides when ChatGPT calls you.
description:
"Use this when the user asks where a sales order is: confirmed, picking, " +
"shipped or invoiced. Read-only. It cannot change, cancel or reprice an order.",
inputSchema: z.object({
orderNumber: z.string().regex(/^SO-\d{6}$/).describe("Sales order number, e.g. SO-004217"),
}),
outputSchema: z.object({
orderNumber: z.string(),
stage: z.enum(["confirmed", "picking", "shipped", "invoiced"]),
promisedDate: z.string(),
lines: z.array(z.object({ sku: z.string(), ordered: z.number(), shipped: z.number() })),
}),
// Explicit booleans, all three. Reviewers check these against behaviour.
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
// Standard MCP Apps key. ChatGPT also reads the alias _meta["openai/outputTemplate"].
_meta: { ui: { resourceUri: ORDER_CARD_URI } },
},
async ({ orderNumber }) => {
const order = await findSalesOrder(orderNumber);
if (!order) {
return { isError: true, content: [{ type: "text", text: `No sales order ${orderNumber}.` }] };
}
return {
// Seen by the model AND the card. Keep it small: it stays in context.
structuredContent: {
orderNumber,
stage: order.stage,
promisedDate: order.promisedDate,
lines: order.lines,
},
// What the model narrates from.
content: [{ type: "text", text: `${orderNumber} is ${order.stage}, promised ${order.promisedDate}.` }],
// Card-only data the model never sees. No trace IDs or internal keys here either.
_meta: { warehouseLabel: order.warehouseDisplayName },
};
},
);
registerAppResource(
server,
"Order status card",
ORDER_CARD_URI,
{ mimeType: RESOURCE_MIME_TYPE },
async () => ({
contents: [
{
uri: ORDER_CARD_URI,
mimeType: RESOURCE_MIME_TYPE,
text: await fs.readFile("dist/order-card.html", "utf-8"), // single-file Vite build
// Empty allowlists: the card fetches nothing. Widen only what you use.
_meta: { ui: { prefersBorder: true, csp: { connectDomains: [], resourceDomains: [] } } },
},
],
}),
);
return server;
}Nilai kembaliannya punya tiga kanal, dan bagian inilah yang paling mudah salah. structuredContent adalah data ringkas yang dibaca model sekaligus UI, dan tetap tersedia bagi model di giliran berikutnya, jadi isinya fakta, bukan berhalaman-halaman baris. content adalah teks yang dinarasikan model. _meta adalah data khusus client yang disembunyikan dari model, berguna untuk label yang hanya untuk tampilan.
Description dan annotations bekerja lebih keras daripada handler-nya. Description adalah cara ChatGPT memutuskan kapan memanggil tool, jadi isinya kapan tool dipakai dan apa yang tidak bisa dilakukannya. Ketiga annotations wajib berupa boolean eksplisit: readOnlyHint true untuk retrieval, destructiveHint false karena tidak ada yang ditimpa, openWorldHint false karena datanya terbatas pada satu tenant ERP privat.
UI-nya adalah resource HTML dengan URI ui:// dan MIME type text/html;profile=mcp-app. Tool menunjuk ke resource itu lewat _meta.ui.resourceUri; ChatGPT juga menerima key lama openai/outputTemplate sebagai alias. Saat tool berjalan, host mengambil resource tersebut, merendernya di frame yang di-sandbox, lalu meneruskan hasil tool lewat postMessage. Class App dari package ext-apps membungkus jembatan itu.
// order-card.ts — bundled into dist/order-card.html
import { App } from "@modelcontextprotocol/ext-apps";
type OrderStatus = {
orderNumber: string;
stage: string;
promisedDate: string;
lines: { sku: string; ordered: number; shipped: number }[];
};
const app = new App({ name: "Order status card", version: "1.0.0" });
let current: OrderStatus | undefined;
// Wrong: connecting first and attaching handlers afterwards. The initial tool
// result can arrive before anyone is listening, and the card renders empty.
// Right: register every handler, then connect.
app.ontoolresult = (result) => {
const order = result.structuredContent as OrderStatus | undefined;
if (order) render(order);
};
document.getElementById("refresh")!.addEventListener("click", async () => {
if (!current) return;
// The card can call its own server's tools without a new chat turn.
const result = await app.callServerTool({
name: "get_order_status",
arguments: { orderNumber: current.orderNumber },
});
render(result.structuredContent as OrderStatus);
});
function render(order: OrderStatus) {
current = order;
// textContent, never innerHTML: structuredContent is untrusted input.
document.getElementById("stage")!.textContent = order.stage;
document.getElementById("promised")!.textContent = order.promisedDate;
const open = order.lines.filter((l) => l.shipped < l.ordered).length;
document.getElementById("open-lines")!.textContent = String(open);
}
app.connect();Dua aturan dari dokumentasi membentuk kode ini. Perlakukan structuredContent sebagai input yang tidak tepercaya, jadi kartu menulis textContent dan tidak pernah innerHTML. Lalu deklarasikan Content Security Policy pada resource: connectDomains untuk apa pun yang di-fetch kartu, resourceDomains untuk script, font, atau gambar yang dimuatnya. Kartu order yang hanya merender hasil tool tidak butuh keduanya, jadi kedua daftar dibiarkan kosong.
Kaitkan resource ui:// hanya dengan tools yang memang perlu merendernya. Di plugin ERP, sebagian besar tool seperti pencarian atau total lebih baik dijawab dengan teks; kartu di setiap pemanggilan mengubah percakapan menjadi dashboard yang tidak diminta siapa pun. Saya memberi versi pada URI, seperti contoh di dokumentasinya sendiri, supaya kartu yang didesain ulang menjadi resource baru, bukan perubahan diam-diam.
Tool menyatakan apa yang bisa dijawab ERP; skill menyatakan bagaimana tugas seorang sales seharusnya berjalan. Skill di bawah mengubah tool status order menjadi rutinitas follow-up order terlambat. Isinya dua field wajib, name dan description yang memberi tahu model kapan mempertimbangkannya, lalu instruksi bernomor yang sederhana.
---
name: late-order-follow-up
description: Use when a sales user asks which of a customer's orders are late, or what to tell a customer about a delayed shipment.
---
# Late order follow-up
1. Call get_order_status for every order number the user gives. Never infer a stage.
2. An order is late when its stage is "confirmed" or "picking" and promisedDate is before today.
3. For each late order, list the lines where shipped is less than ordered.
4. Draft a short customer update: what shipped, what has not, and the original promised date.
Do not promise a new date. The tool does not return one, so any date you write is invented.
5. If the tool returns an error, say the order number was not found and ask the user to check it.
Do not retry with a "corrected" number you made up.Nilai terbesarnya ada pada larangan. Langkah empat melarang menjanjikan tanggal kirim baru, karena tool tidak pernah mengembalikannya dan model yang percaya diri akan mengarang tanggal yang terdengar masuk akal untuk customer sungguhan. Langkah lima mencegahnya retry dengan nomor order tebakan. Inilah failure mode yang patut diantisipasi dari model mana pun yang terhubung ke data ERP, dan skill adalah tempat termurah untuk menutupnya.
Jalankan server secara stateless: server dan transport baru untuk tiap request, tanpa session ID. Begitulah contoh ext-apps ditulis, dan artinya replica mana pun di belakang load balancer bisa menjawab panggilan apa pun. Sebelum ChatGPT melihatnya, hubungkan MCP Inspector ke endpoint /mcp lokal dan panggil tool secara manual dengan nomor yang valid, nomor yang tidak dikenal, dan nomor yang formatnya salah.
// main.ts
import { createMcpExpressApp } from "@modelcontextprotocol/express";
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import { createServer } from "./server.js";
const app = createMcpExpressApp({ host: "0.0.0.0" });
app.all("/mcp", async (req, res) => {
const server = createServer();
const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000);
# then, in another terminal, before ChatGPT ever sees it:
# npx @modelcontextprotocol/inspector -> connect to http://localhost:3000/mcpAkses developer mode tidak sama di setiap paket. Gradually.ai melaporkan bahwa custom MCP apps di Pro hanya berjalan di web dan terbatas pada aksi read dan fetch, sementara dukungan MCP penuh termasuk aksi write sedang digulirkan dalam beta di Business, Enterprise, dan Edu. Rancang plugin ERP dengan prinsip read-first, dan pastikan batas terbaru untuk paket Anda sebelum menjanjikan write tool kepada siapa pun.
Submission dilakukan di platform dashboard OpenAI pada menu Plugins, lewat Upload new or existing plugin. Pemeriksaan otomatis dijalankan terhadap package lebih dulu; Anda meninjau temuannya, melengkapi yang dibutuhkan tim review, lalu men-submit draft-nya. Siapkan hal-hal berikut sebelum mulai, karena masing-masing akan memblokir form.
Persetujuan tidak otomatis mempublikasikan apa pun. Anda membuka versi yang disetujui lalu memilih Publish plugin saat sudah siap. Guidelines-nya adalah bagian yang biasanya membuat plugin ERP perlu dikerjakan ulang: tools tidak boleh meminta seluruh riwayat percakapan atau field konteks sekadar berjaga-jaga, dan response tidak boleh membawa session ID, trace ID, request ID, atau metadata logging. API ERP mengembalikan semua itu secara default, jadi petakan response-nya ke bentuk yang sempit dengan sengaja.
Lapisan UI-nya adalah standar terbuka MCP Apps, dipublikasikan sebagai @modelcontextprotocol/ext-apps dengan entry point /server dan /react. Repository-nya mencantumkan ChatGPT, Claude, VS Code, Goose, Postman, dan MCPJam sebagai host yang merendernya, jadi kartu order yang dibangun di sini tidak terkunci pada satu produk chat.
Namanya berganti tiga kali; kontraknya tidak. Bangun MCP server yang sempit dan read-only dengan annotations yang jujur, kembalikan UI hanya ketika gambar lebih jelas daripada kalimat, letakkan workflow beserta larangannya di skill, dan perlakukan checklist submission sebagai bagian dari build, bukan urusan administrasi di akhir.