Skip to main content
Lesson 4 of 14 · ChatGPT Apps SDK and Plugins: MCP Servers and Interfaces

Your First Plugin

Lesson 4 of 14 Updated November 3, 2026 Developer
On this page
  1. Two Steps
  2. Step 1: A Personal Plugin in Five Minutes
  3. Step 2: Your Own Server
  4. Reload After Every Change
  5. Key Takeaways

Lesson rewritten November 3, 2026 based on the Quickstart, MCP server and UI quickstart, and Connect and test your plugin pages in the OpenAI Plugins documentation. The previous “weather app in 30 minutes” example relied on a non-existent SDK. We verified npm package versions on November 3, 2026: @modelcontextprotocol/ext-apps 2.x builds on MCP SDK v2, while OpenAI’s examples use v1. Sources: Quickstart · MCP server and UI quickstart · Connect and test.

Two Steps

First, you’ll connect an existing MCP server to see the full ChatGPT workflow without writing a single line of code. Next, you’ll write your own server, test it locally, and connect it the same way.

Step 1: A Personal Plugin in Five Minutes

OpenAI provides a public, unauthenticated example MCP server that exposes a single read-only tool: roll_dice.

  1. Go to ChatGPT Plugins, select the + button, then choose Add custom MCP server.
  2. Enter a name and set the Server URL to https://tinymcp.dev/api/moldy-aloof-zettabyte/mcp. Select No authentication.
  3. Read the risk warning, select I understand and want to continue, then click Create as a plugin.
  4. The plugin appears in your personal plugins list: open it and install it using the + button.
  5. Return to the ChatGPT home screen, switch from the Chat tab to Work, start a new conversation, type @, and select your plugin.
  6. Ask it to roll a twenty-sided die: it should call roll_dice exactly once with sides set to 20 and return a value between 1 and 20.

Don’t stop at the happy path. Try different die sizes, invalid values, and requests that shouldn’t trigger any tool. This reflex drives the rest of the course: when the model picks the wrong tool or varies its arguments, you need to refine the tool’s description and schema. Also note that your account or workspace policies govern adding custom MCP servers: in an organization, this option may be restricted.

Step 2: Your Own Server

Install the Official SDKs

Official protocol SDKs exist for TypeScript (@modelcontextprotocol/sdk) and Python (mcp). This course follows OpenAI’s examples, written in TypeScript using SDK v1. Pin your versions: as of September 2026, @modelcontextprotocol/ext-apps 2.x—the interface package you’ll add in Lesson 6—relies on the new v2 SDK packages (@modelcontextprotocol/server…), which are incompatible with v1 imports.

mkdir notes-plugin && cd notes-plugin
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/sdk@^1.29 @modelcontextprotocol/ext-apps@^1.7 zod

npm pkg set type=module adds "type": "module" to package.json, required for the server’s import statements.

Write the Server

This server fits in a single file, server.js. It keeps notes in memory and exposes two tools: one for reading and one for writing, separated as recommended in Lesson 3.

import { createServer } from "node:http";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";

const notes = []; // example in-memory data: replace with your own storage
const schemaNotes = {
  notes: z.array(z.object({ id: z.string(), text: z.string() })),
};

function creerServeur() {
  const server = new McpServer({ name: "notes-demo", version: "0.1.0" });

  server.registerTool(
    "list_notes",
    {
      title: "List notes",
      description: "Use when the user wants to review notes saved with this plugin.",
      inputSchema: {},
      outputSchema: schemaNotes,
      annotations: { readOnlyHint: true, openWorldHint: false, destructiveHint: false },
    },
    async () => ({
      structuredContent: { notes },
      content: [{ type: "text", text: `${notes.length} note(s) saved.` }],
    })
  );

  server.registerTool(
    "add_note",
    {
      title: "Add a note",
      description: "Use when the user asks to save a short note.",
      inputSchema: { text: z.string().min(1).max(500) },
      outputSchema: schemaNotes,
      annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: false },
    },
    async ({ text }) => {
      notes.push({ id: `note-${notes.length + 1}`, text });
      return {
        structuredContent: { notes },
        content: [{ type: "text", text: "Note saved." }],
      };
    }
  );

  return server;
}

const PORT = Number(process.env.PORT ?? 8787);

createServer(async (req, res) => {
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
  if (url.pathname !== "/mcp") {
    res.writeHead(404).end("Not Found");
    return;
  }

  // Stateless mode: new server and transport per request
  const server = creerServeur();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
    enableJsonResponse: true,
  });
  res.on("close", () => {
    transport.close();
    server.close();
  });

  try {
    await server.connect(transport);
    await transport.handleRequest(req, res);
  } catch (error) {
    console.error("MCP Error:", error);
    if (!res.headersSent) res.writeHead(500).end("Internal server error");
  }
}).listen(PORT, () => console.log(`MCP server ready on http://localhost:${PORT}/mcp`));

Three details matter. Each tool declares a title, an intent description, schemas, and exact annotations: this is its “profile” for the model. Each result combines structuredContent, reusable by the model, and content, a short text message. Finally, any address other than /mcp returns a 404: OpenAI’s example does this for OAuth discovery routes you aren’t using yet, preventing ChatGPT from showing 502 errors during your unauthenticated tests. That same example adds a response for the preliminary CORS request (OPTIONS) and a GET / for health probes; include these if your hosting expects them.

Test Locally with MCP Inspector

node server.js
npx @modelcontextprotocol/inspector@latest

In the Inspector, choose Streamable HTTP, enter http://localhost:8787/mcp, and connect. Verify that initialization succeeds, that both tools appear with their schemas and annotations, then call them with valid and invalid inputs—an empty text string must be rejected by the schema.

Expose the Server and Connect It

ChatGPT must reach your server from the internet. During development, a tunnel suffices—the documentation cites ngrok:

ngrok http 8787

You get a public HTTPS address. Connect it as in Step 1, including the /mcp path (for example https://<subdomain>.ngrok.app/mcp), install the resulting plugin, then invoke it with @: “Save a note: call the vendor Monday.” ChatGPT displays the data exchanged with the tool, letting you verify inputs and outputs.

A development tunnel works only for trials: publishing requires a stable, public HTTPS endpoint (Lesson 5).

Reload After Every Change

ChatGPT caches your server’s metadata. After any change to the name, description, schema, annotation, authentication, or interface resource: redeploy or restart the server, open the connection at chatgpt.com/plugins, select Refresh, verify that the advertised metadata has changed, then open a new conversation to re-run your tests. Skipping this step means testing the old version without realizing it.

Key Takeaways

  • Create a personal plugin via Add custom MCP server, then invoke it with @ in ChatGPT
  • Official SDKs: @modelcontextprotocol/sdk (TypeScript) and mcp (Python); pin v1 with ext-apps 1.x to follow OpenAI’s examples
  • Minimal server: McpServer, registerTool, streamable HTTP transport on /mcp
  • Use MCP Inspector for local testing, a tunnel for ChatGPT trials
  • After any metadata change: Refresh, then start a new conversation