How to: build your first ChatGPT app

In this post I’ll walk you through building a simple todo list app inside ChatGPT. You’ll see how to wire tool endpoints, widget UIs, and deploy it so a user in ChatGPT can see and interact with your UI. I’m starting with a Gadget template that handles ChatGPT’s OAuth and MCP (model context protocol) server setup so I can just focus on building my data models, actions, widgets, and tool calls that power my app.

This is still very early after the OpenAI Apps SDK release! This post will be updated as we continue to explore the SDK’s APIs. Important features for actual production apps, like data multi-tenancy, are not currently included (but will be soon!).
To build apps for ChatGPT, you need to be on a paid plan, and OpenAI developer mode needs to be enabled.
If you want to skip the build and go straight to the end result, you can fork the app here:
Prefer a video? You can watch the build here:
The big picture
Before we jump into code, here’s how all the pieces will fit:
- Your Gadget app is where your data, business logic, and widget UI live.
- You expose tool actions via the MCP protocol (e.g. listTodos, addTodo, completeTodo) that ChatGPT can call.
- You define resource endpoints (React components packaged as UI) that ChatGPT will call through an associated tool to fetch and embed inside the chat.
- ChatGPT sees your connector (you register it via Apps & Connectors), knows your tools and resources, and can call them or embed them.
- ChatGPT can call your tool endpoints (via window.openai.callTool), set and get widget state, and trigger follow-up messages.
A user in ChatGPT can ask “Show me my todos,” see the UI embedded, add or complete tasks, and those changes will make sure of ChatGPT’s built in widget storage that acts as a chat-specific cache, and they will be persisted in your database for use in additional chats.
Building a todo list for ChatGPT
Here’s how to build it. I’ll walk you step by step, and drop code snippets so you can follow along.
1. Fork the Gadget template which comes with auth, an MCP server, and a sample widget already set up.
2. Connect your app in ChatGPT in Settings > Apps & Connectors.
- Your app URL should be of the format <inline-code>https://<app-name>--<environment>.gadget.app/mcp<inline-code>
- For example, <inline-code>https://my-todo-list--development.gadget.app/mcp<inline-code>
3. Install your app in ChatGPT. You can choose to use OAuth authentication, which is built in to the template, or install with no authentication to use the widget you will build.
4. After installing, you can start a new chat, add your app as a source in the chat, and ask ChatGPT to "Use my app to say hello". A “hello, world” widget should print out in the chat!
Add a todo data model
This app already has an MCP server set up. It is serving up the widget via a <inline-code>sayHello<inline-code> tool call made to Gadget from ChatGPT. All the boilerplate setup is handled, all that is needed now is a data model for your todos, a widget to render in ChatGPT, and the tool actions required to tie data and UI together.
1. Add a <inline-code>todo<inline-code> model to your Gadget app at <inline-code>api/models<inline-code>.
- Add an <inline-code>item<inline-code> field, with the <inline-code>string<inline-code> field type.
- Add an <inline-code>isComplete<inline-code> field, with the <inline-code>boolean<inline-code> field type, with a default value of <inline-code>false<inline-code>.
The model will also have an <inline-code>id<inline-code> field, <inline-code>createdAt<inline-code> and <inline-code>updatedAt<inline-code> timestamps, and it will be related to the <inline-code>user<inline-code> model.
Update your MCP server
6. Add tool actions to the MCP server. Copy and paste the following into <inline-code>api/mcp.ts<inline-code>:
MCP server details
The code in <inline-code>api/mcp.ts<inline-code> defines your MCP server and the tools calls that will be made to load your todo list widget, add todos, and mark todos as completed.
The tool calls for <inline-code>addTodo<inline-code> and <inline-code>completeTodo<inline-code> are similar. They both have <inline-code>inputSchema<inline-code> defined, which is how you can define the data passed in to a tool call from your widget. The <inline-code>_meta: { "openai/widgetAccessible": true }<inline-code> property is required to make the action accessible from your widget.
Both of these tools also use the auto-generated Gadget API client to <inline-code>create<inline-code> and <inline-code>update<inline-code> todos. A CRUD API is created for you when you add new data models in Gadget.
Finally, <inline-code>structuredContent<inline-code> defines what will be returned by the action. Both both of these actions, the single created or updated todo is returned. The current todo widget is not optimistically adding todos to the UI, it waits for the created todo to be returned via <inline-code>structuredContent<inline-code>.
The <inline-code>listTodos<inline-code> tool call uses the <inline-code>api<inline-code> client to read a page of todos and return them as <inline-code>structuredContent<inline-code>. (Pagination is built into the API, and could be done with another tool call.)
The <inline-code>_meta<inline-code> definition is important here. <inline-code>"openai/outputTemplate": "ui://widget/widget.html"<inline-code> associated this tool call with the URI used for the todo resource defined on the MCP server. This is what allows ChatGPT to fetch your widget.
Finally, the <inline-code>todos<inline-code> resource is where your widget code and the URI referenced by the tool action are defined. This combination of a resource and a tool call is what is required to render a widget in ChatGPT.
Time to update the widget code!
Build your frontend widget
1. Copy and paste the following into <inline-code>api/widget.html<inline-code>:
Your widget is rendered in an iframe, and additional APIs have been added to <inline-code>window.openai<inline-code> that let you do things like read data passed into the widget from the initial tool call, persist UI state, and make additional tool calls.
<inline-code>window.openai.toolOutput<inline-code> is how <inline-code>structuredContent<inline-code> passed in by the <inline-code>listTodos<inline-code> tool call is read in the frontend. <inline-code>toolOutput<inline-code> is only defined when the widget is initially rendered!
<inline-code>window.openai.widgetState<inline-code> is persisted widget state. On a refresh or if a user leaves and comes back to your chat, this state can be used for an initial render state so the user can continue from where they left off.
<inline-code>window.openai.callTool(...)<inline-code> is how the <inline-code>addTodo<inline-code> and <inline-code>completeTodo<inline-code> actions are called from your widget.
This is all that’s required to build your app! Now you can test it out.
Testing your todo list
To test your todo list:
- Go back to your app in ChatGPT’s Settings and Refresh. You should see your todo actions listed after the refresh is complete.
- Open a new chat, add your tool as a source and ask ChatGPT to “Use my app to list my todos”. (It is helpful to be explicit about using the app when testing.)
- The todo list should appear in ChatGPT! Try adding a todo, and marking it as complete.
- Adding a todo is done in a non-optimistic way right now! We will make this smoother when we update this tutorial to use React widgets.
- When you do so, notice that data is persisted to your Gadget db at <inline-code>api/models/todo/data<inline-code>
- Adding a todo is done in a non-optimistic way right now! We will make this smoother when we update this tutorial to use React widgets.
- Try refreshing you chat, or leave and come back. You todo list should re-appear with its current state intact.
Congrats! You’ve built an app (and not just any app, a todo list) in ChatGPT!
What’s next?
We’re going to continue to update our template and this tutorial as we build out more tooling for ChatGPT apps in Gadget.
The following updates are coming soon:
- Build widgets in React, using our Vite plugin.
- Data multi-tenancy. Connect all data to a signed-in <inline-code>user<inline-code>.
- Managed OAuth. Right now, all the OAuth code exists in your app. We are looking to build a ChatGPT app connection that takes care of OAuth, similar to Gadget’s Shopify and BigCommerce connections.
Have questions?
We’ll be updating our docs with more info as we get better acquainted with building ChatGPT apps.
Hop in our developer Discord if you have any questions for us. We’re always happy to help!