On DevDay 2025, OpenAI announced apps in ChatGPT and the new Apps SDK , and my mind immediately started racing…
ChatGPT now has over 800 million users, with unbelievably good retention rates. This makes it a prime platform for builders to use as a launching pad for new ideas and products. ChatGPT Apps is poised to be the App Store of the AI Age, and we are still early enough that very few people are looking.
Let’s be among the first to take advantage of what the Apps SDK has to offer.
Note: This is a technical blog, aimed at developers, and assumes experience in both Python and TypeScript.
Pre-requisites
You will need access to ChatGPT’s Developer Mode, which is available at any of its paid tiers. The cheapest one is ChatGPT Plus for 20 bucks.
git to clone the starting template.
mise as a dev env manager. `mise` will take care of installing `uv` and `node`, which are both required for running the app.
ngrok so ChatGPT can access your development build.
Cloning the template
I made a template that takes care of all the boilerplate so we can get straight to the good parts. Clone it with:
git clone https://github.com/ajac-zero/custom-chatgpt-app.gitThe template consists of a Python MCP server, with a nested Node app that builds React components into js/css bundles. The MCP server is heavily modified to follow the ChatGPT App spec, which builds on top of the standard MCP spec. The purpose of the `superset` spec is to allow the MCP server to also return appropriate HTML for each tool in addition to the normal JSON-RPC response.
Here is a breakdown of the important topology:
src/custom-chatgpt-app/widgets → In this Python module, you will write the backend part of every widget. You must then import it in widgets/__init__.py and add it to the widget list. This will register your component within the MCP server.
ui/src/components → In this subdirectory, you will find the frontend part of every widget. You can use special hooks `useToolInput` and `useToolOutput` to access the input and output of the backend, respectively. Every `tsx` file at the root of this subdirectory will get bundled so the backend can serve it. `tsx` in subdirectories will be ignored by the build script.
Those are basically the only two directories that matter if you want to add your own components!
Adding a custom component
Let’s add our first custom component. It will be a `weather` widget that will display the current weather in a given location. The workflow to add new components is as follows:
Create a frontend React component for the widget
Run the build script to bundle the component
Create a backend Python model that implements the logic for the widget
Load the component bundle into the backend
Register the model in the MCP server
Simple, right? In fact, it is so simple, even a coding agent could do it. That is why I included an `AGENTS.md` file in the template, because for simple components like this, we can use any coding agent like Claude Code, Gemini CLI, or Codex.
In this case, I will open up Codex and give it this prompt:
Can you please create a new Weather widget? It should show the current weather in a given city. Use some free weather API.
After a few minutes of thinking, it made three changes to the project:
Added the `weather.tsx` component.
Added the `weather.py` module.
Added the new `weather_widget` from `weather.py` to the MCP server.
You can look at the full code changes in this GitHub commit.
Testing our new component
Now that our component is ready to go, let’s use it within ChatGPT.
In a terminal, run `mise dev`. This will start up the MCP server.
In a different terminal window, while `mise dev` is running, run `ngrok http 8000`. This will provide you with a public URL that forwards to your server.
In ChatGPT, go to Settings > Apps & Connectors > Advanced Settings and turn on Developer Mode. Remember, this option will only appear if you have a paid ChatGPT subscription.
Now go to Settings > Apps & Connectors > Create and fill in the form with these values:
Name: Weather
Description: Get live weather updates for cities
MCP Server URL: {your-public-url}/mcp
Authentication: No authentication
Accept the custom component risk disclaimer
Press create, and it will briefly connect to your MCP server to register the available tools.
Now your component is all set up! Let’s look add the final result:

