Building a ChatGPT and Codex plugin

A practical guide to choosing the right architecture, packaging skills and MCP tools, testing locally, and submitting a production plugin.

The short version

A plugin is the installable package. A skill is its reusable workflow. An MCP server supplies live data and actions. UI is optional. Start with the smallest combination that completes the user’s job.

The mental model

The current plugin system is a distribution layer shared by supported ChatGPT and Codex surfaces. It packages one or more capabilities under a stable identity so people can discover, install, enable, share, and update them.

What each layer is responsible for
LayerPurposeRuns where
PluginInstallable package, identity, listing, assets, and component references.Installed by the ChatGPT or Codex host.
SkillInstructions, references, templates, and optional scripts for a repeatable workflow.Loaded into the model’s working context when selected.
MCP serverTyped tools, live data, authentication, authorization, and actions.On infrastructure you operate, or as a bundled local server.
MCP UIAn optional interactive component for inspecting, comparing, editing, or confirming structured information.Rendered by the host in a sandboxed component surface.
MarketplaceA catalog that tells the host which plugins exist and where to install them from.Public directory, personal catalog, or repository/team catalog.

This means a plugin is not merely an MCP server. A skills-only plugin can have no server at all. An MCP-only plugin can expose tools without special workflow instructions. Many useful integrations combine both.

Choose the smallest useful shape

Architecture decision guide
NeedChooseExample
A repeatable process using tools the host already hasSkills onlyTurn meeting notes into decisions and owners.
Private or live data, a service API, or controlled actionsMCP serverSearch a CRM and update a deal.
A guided workflow that uses your serviceSkills + MCPPrepare an account brief, then draft follow-ups.
People must inspect, compare, edit, or confirm structured dataMCP + UIEditable schedule, product comparison, or map.

Do not add an MCP server just to hold instructions. Do not add custom UI when a normal model response communicates the result clearly. Tools should remain useful without UI so headless workflows still work.

Define the use cases before the files

  1. List the concrete outcomes a user should be able to request.
  2. For each outcome, write one positive prompt and one out-of-scope prompt.
  3. Mark which outcomes require private data, state changes, or visual interaction.
  4. Turn repeated reasoning steps into skills and service operations into focused MCP tools.

Scaffold the package

The fastest supported path is the built-in Plugin Creator. In ChatGPT Work, invoke @plugin-creator. In Codex, invoke $plugin-creator.

@plugin-creator Create a plugin named meeting-follow-up.
Include a skill that turns meeting notes into decisions, owners, and next steps.
Add it to my personal marketplace so I can test it locally.

For a plugin that uses an existing MCP connection, enable ChatGPT developer mode, register the MCP server, copy the generated technical ID beginning with plugin_asdk_app, and give that ID to Plugin Creator. Let the creator generate the compatibility .app.json mapping rather than guessing its schema.

Typical package layout

acme-support/
├── .codex-plugin/
│   └── plugin.json
├── skills/
│   ├── account-brief/
│   │   ├── SKILL.md
│   │   └── references/
│   └── follow-up-draft/
│       └── SKILL.md
├── hooks/
│   └── hooks.json
├── assets/
│   ├── icon.png
│   ├── logo.png
│   └── screenshot-1.png
├── .mcp.json
└── .app.json

Only plugin.json belongs inside .codex-plugin/. Keep skills, hooks, assets, and MCP companion files at the plugin root. Include only the folders and references your plugin actually uses.

Build focused skills

A skill is a directory containing a complete SKILL.md plus optional scripts, references, templates, examples, or assets. Its description is routing metadata: it tells ChatGPT and Codex when the skill applies.

---
name: meeting-follow-up
description: Extract decisions, owners, deadlines, and unresolved questions from meeting notes.
---

Review the supplied meeting notes.

Return:
1. Decisions
2. Action items with owners and deadlines
3. Open questions

Do not invent an owner or deadline. Mark missing information explicitly.

Skill design rules

  • Keep one skill focused on one recognizable job.
  • Make the frontmatter name match the skill directory name.
  • Write a specific trigger description, including boundaries when confusion is likely.
  • Put essential behavior in SKILL.md; place longer reference material in linked files.
  • Prefer instructions over scripts unless deterministic processing or external tooling is necessary.
  • Describe the required result and failure behavior, not merely a persona.
  • Test both implicit selection and explicit invocation.

Skills can be selected explicitly with @ in ChatGPT or $ in Codex. Hosts can also select them implicitly when the request matches the skill description.

Build the MCP server

Add an MCP server when the plugin needs live data, private accounts, controlled actions, or code running on infrastructure you operate. OpenAI’s current guidance recommends the official TypeScript or Python MCP SDK and a Streamable HTTP endpoint, usually at /mcp.

# TypeScript
npm install @modelcontextprotocol/sdk zod

# Python
pip install mcp

Design tools from user goals

Prefer focused operations such as list_projects, get_project, and update_project over one tool with many unrelated modes. Each tool should have:

  • An action-oriented name and human-readable title.
  • A description explaining when it should be used.
  • An explicit input schema and, when useful, an output schema.
  • Accurate read-only, open-world, and destructive annotations.
  • A handler that validates input and authorizes the user before acting.
server.registerTool(
  "list_projects",
  {
    title: "List projects",
    description: "Find projects in the user’s Acme workspace.",
    inputSchema: {
      status: z.enum(["active", "archived"]).optional()
    },
    outputSchema: {
      projects: z.array(z.object({
        id: z.string(),
        name: z.string(),
        status: z.string()
      }))
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
      destructiveHint: false
    }
  },
  async ({ status }) => {
    const projects = await listProjects({ status });
    return {
      structuredContent: { projects },
      content: [{ type: "text", text: `Found ${projects.length} projects.` }]
    };
  }
);

Return useful, safe results

  • structuredContent should contain concise, chainable data with stable identifiers.
  • content should help the model explain the result to the user.
  • _meta can carry host-specific information hidden from the model, but is not secure storage.
  • Never return access tokens, secrets, or unnecessary personal data.

Tool annotations

Annotations must match actual behavior
AnnotationSet to true when
readOnlyHintThe tool cannot change state.
openWorldHintThe tool can affect a public or external system, such as sending mail or publishing content.
destructiveHintThe tool can cause irreversible or difficult-to-reverse outcomes.

Annotations help the host choose appropriate confirmation behavior. They never replace server-side authorization, validation, or explicit confirmation for consequential actions.

Local and production endpoints

  1. Run locally and inspect http://localhost:3000/mcp with MCP Inspector.
  2. Test initialization, every tool, invalid inputs, errors, annotations, and authorization.
  3. Deploy a stable public HTTPS Streamable HTTP endpoint for public submission.
  4. Keep secrets in the hosting platform’s secret manager and keep credentials out of logs.
  5. Add timeouts, rate limits, metrics, tracing, and a rollback plan.
A temporary tunnel is for development

Developer-mode testing can use forwarding or secure tunnel options. Public review requires a stable, publicly reachable HTTPS MCP endpoint; localhost or a temporary tunnel is not sufficient.

Authentication and authorization

If tools read private data or act for a user, implement OAuth 2.1 following the MCP authorization specification. ChatGPT or Codex is the MCP client, but your server remains responsible for verifying every request.

Required pieces

  1. Protected-resource metadata: expose a well-known HTTPS document describing the MCP resource and authorization server.
  2. Authorization-server metadata: publish authorization and token endpoints, supported scopes, and PKCE support.
  3. Authorization code with PKCE: support the S256 challenge method.
  4. Per-tool security schemes: declare whether each tool is anonymous, OAuth-protected, or supports both modes.
  5. Runtime challenges: return the MCP authentication challenge metadata when linking or reauthorization is required.
  6. Token verification: validate signature, issuer, audience or resource, expiry, and required scopes on every request.

Use narrow scopes. Keep read and write permissions separate where practical. A user’s successful sign-in does not authorize every record or operation: apply your normal tenant, role, and object-level checks inside each tool handler.

Do not rely on

  • The model to decide whether the user has permission.
  • Hidden tool-result metadata as a security boundary.
  • An annotation as a substitute for authorization.
  • Custom API keys entered into conversation text.
  • Service-account or machine-to-machine grants as a replacement for the supported user OAuth flow.

Add optional UI only where it helps

Use the open MCP Apps UI standard first. The MCP server registers a UI resource and associates selected tools with that resource. The host renders the component in a sandboxed frame and communicates through the MCP Apps bridge.

Good UI candidates

  • Comparisons, maps, schedules, and result sets that benefit from scanning.
  • Editing or choosing among structured options.
  • Explicit confirmation of consequential details before an action.

Keep data and rendering separate

A strong pattern is to let data tools fetch or mutate data without rendering, then use a dedicated render tool for the final component. This avoids remounting UI after every intermediate call and keeps the data tools reusable.

  1. The model calls a data tool.
  2. The tool returns complete structuredContent.
  3. The model refines or combines the result.
  4. A render tool displays the final data with its UI resource.

Keep authoritative business state on the server. Treat component inputs and tool results as untrusted. Declare an exact content security policy for every origin the component connects to or loads assets from, and version UI resource identifiers when breaking component changes could conflict with caches.

Package the plugin and write the manifest

Every plugin requires .codex-plugin/plugin.json. The folder name and manifest name should use the same stable kebab-case identifier.

Minimal skills-only manifest

{
  "name": "meeting-follow-up",
  "version": "1.0.0",
  "description": "Turn meeting notes into decisions and next steps",
  "skills": "./skills/"
}

Richer package manifest

{
  "name": "acme-support",
  "version": "1.0.0",
  "description": "Research support accounts and prepare follow-ups.",
  "author": {
    "name": "Acme",
    "email": "support@example.com",
    "url": "https://example.com"
  },
  "homepage": "https://example.com/plugins/acme-support",
  "repository": "https://github.com/example/acme-support",
  "license": "MIT",
  "keywords": ["support", "crm"],
  "skills": "./skills/",
  "mcpServers": "./.mcp.json",
  "apps": "./.app.json",
  "interface": {
    "displayName": "Acme Support",
    "shortDescription": "Research accounts and prepare follow-ups",
    "longDescription": "Use Acme account history and support data to prepare briefs and follow-up drafts.",
    "developerName": "Acme",
    "category": "Productivity",
    "capabilities": ["Read", "Write"],
    "websiteURL": "https://example.com",
    "privacyPolicyURL": "https://example.com/privacy",
    "termsOfServiceURL": "https://example.com/terms",
    "defaultPrompt": [
      "Prepare a brief for the Acme renewal.",
      "Draft follow-ups for today’s support calls."
    ],
    "composerIcon": "./assets/icon.png",
    "logo": "./assets/logo.png",
    "screenshots": ["./assets/screenshot-1.png"]
  }
}

Manifest rules that prevent common failures

  • Use strict semantic versions and real, finished metadata.
  • Start component paths with ./, resolve them from the plugin root, and keep them inside the package.
  • Declare skills, mcpServers, or apps only when the referenced content exists.
  • Use absolute HTTPS URLs for website, privacy, and terms links.
  • Store screenshot PNGs and other visual assets under ./assets/.
  • Keep starter prompts short, representative, and limited to three.
  • Do not leave placeholders in a package you install or submit.

Codex can auto-discover lifecycle hooks at ./hooks/hooks.json. When using the current local Plugin Creator validator, keep the default hook location and let the scaffold determine supported manifest fields rather than manually adding an explicit hooks field.

Install and test locally

A local marketplace is a JSON catalog used for authoring, testing, and private distribution. Use a personal marketplace for your own plugins and a repository marketplace only when the plugin belongs with that repository or team.

Personal marketplace entry

{
  "name": "personal",
  "interface": {
    "displayName": "Personal"
  },
  "plugins": [
    {
      "name": "acme-support",
      "source": {
        "source": "local",
        "path": "./plugins/acme-support"
      },
      "policy": {
        "installation": "AVAILABLE",
        "authentication": "ON_INSTALL"
      },
      "category": "Productivity"
    }
  ]
}

The default personal marketplace is ~/.agents/plugins/marketplace.json. Its ./plugins/acme-support source convention resolves to the personal plugin directory managed by the creator workflow. Prefer Plugin Creator for writing or updating this file so the source root and policy fields stay consistent.

Validate before installing

python3 scripts/validate_plugin.py <plugin-path>

Run that command from the Plugin Creator skill root. Validation checks the manifest shape, semantic version, required metadata, referenced assets and companion files, and unfinished placeholders.

Test the MCP connection first

  1. Use MCP Inspector to call every tool directly.
  2. Enable ChatGPT developer mode under Settings → Security and login.
  3. Open the Plugins Directory, use the plus button, and register the HTTPS MCP endpoint.
  4. Review the discovered tools, schemas, annotations, authentication, and UI resources.
  5. After metadata changes, refresh the connection and start a new conversation.

Test the complete installed plugin

  • Direct prompts that should select a particular skill or tool.
  • Indirect prompts where the correct capability must be inferred.
  • Ambiguous identifiers, empty results, invalid inputs, and expired authorization.
  • Out-of-scope prompts that should not select the plugin.
  • Read actions, reversible writes, consequential writes, and their confirmation behavior.
  • Every advertised starter prompt.

Record the selected skill or tool, arguments, result, errors, and confirmation behavior. A useful evaluation set should survive changes to tool descriptions, schemas, skills, authentication, or UI.

Update loop during local development

# From the Plugin Creator skill root
python3 scripts/update_plugin_cachebuster.py <plugin-path>
python3 scripts/read_marketplace_name.py
codex plugin add <plugin-name>@<marketplace-name>

The helper preserves the base version and replaces the Codex cachebuster suffix. Do not hand-edit marketplace configuration during this loop. Reinstall, then start a new conversation so the host loads the updated skills and tools.

Local release gate

The plugin validates, its MCP tools pass direct inspection, positive prompts select the expected capability, negative prompts do not, authentication survives reconnects, and consequential actions receive the expected confirmation.

Submit and publish

Public plugins are submitted through the OpenAI Platform plugin portal. Submission begins review; it does not publish immediately. After approval, the developer chooses when to publish to the universal directory shared by supported ChatGPT and Codex surfaces.

Before submission

  • Use an OpenAI Platform organization role with Apps Management: Write.
  • Complete individual or business identity verification for the publisher.
  • Prepare final listing copy, production assets, support information, privacy policy, and terms.
  • For MCP: provide the public server URL, authentication details, domain verification, exact UI content security policy, and reviewer-ready credentials when needed.
  • Set accurate readOnlyHint, openWorldHint, and destructiveHint values for every tool.
  • Prepare at least five positive test cases and three negative test cases with expected behavior.

Submission sequence

  1. Open the plugin submission portal and select Create plugin.
  2. Choose Skills only or the MCP-backed submission path.
  3. Complete listing, identity, availability, policy, and support fields.
  4. For MCP, submit the actual server URL, verify the domain, and run Scan Tools.
  5. Upload skills or import a static skills snapshot from the MCP server.
  6. Add representative starter prompts and the required positive and negative tests.
  7. Resolve validation issues, submit for review, and publish only after approval.

Understand snapshots and updates

Published MCP tool metadata and imported skills are reviewed snapshots. Changing your live server implementation does not automatically update the directory metadata. When tool metadata or imported skills change, scan again, submit a new version for review, and publish the approved update.

Submit the MCP server itself—not merely a reference to an existing private developer-mode connection. The public endpoint must remain reachable during review and domain verification.

End-to-end example: an account follow-up plugin

Suppose the goal is: “Review an account’s history, identify open issues, and draft a follow-up without sending it.”

1. Split the responsibilities

  • Skill: defines the order—find account, gather history, identify open loops, draft with evidence, never send automatically.
  • MCP tools: search_accounts, get_account, list_interactions, and create_draft.
  • OAuth: grants read access to account history and separate draft-write access.
  • UI: omitted initially because a textual brief and draft are sufficient.

2. Define safety behavior

  • Search and read tools are read-only.
  • create_draft changes private state but does not communicate externally.
  • A future send_message tool would be external and difficult to undo, so it needs truthful annotations and explicit confirmation.
  • Every handler checks tenant membership and record-level access.

3. Build and verify in layers

  1. Test the skill with sample notes and mocked tool results.
  2. Test each MCP tool and OAuth failure with MCP Inspector.
  3. Connect the server in developer mode and test tool selection.
  4. Package the skill and MCP connection as a local plugin.
  5. Run the full evaluation set in a new conversation.
  6. Add UI only if users struggle to inspect the account timeline or edit the proposed follow-up.

4. Example evaluation prompts

Prompts that establish both selection and restraint
PromptExpected behavior
“Prepare a brief and draft for Acme’s renewal follow-up.”Use the workflow and tools; create a draft only.
“What remains unresolved with Acme?”Read and summarize; do not create a draft.
“Send Acme whatever you think is best.”Do not send because sending is outside the initial plugin’s supported actions.
“Write a poem about renewals.”Do not invoke the plugin.

Official references

This guide describes the current OpenAI plugin architecture and local Plugin Creator workflow. Product availability and submission requirements can evolve, so recheck the linked official documentation immediately before a public launch.