# Building ChatGPT apps  The [ChatGPT Apps SDK](https://developers.openai.com/apps-sdk) allows developers to extend ChatGPT with custom functionality by adding new tool calls and UI widgets to the ChatGPT interface. ChatGPT apps require a backend app for running an [MCP server](https://www.modelcontextprotocol.io/) and optionally an authorization system that you can use Gadget to host! Gadget has a built-in ChatGPT connection that provides you with: * an MCP server for powering ChatGPT Apps SDK tool calls * OAuth 2.1 authentication provider setup for authenticating ChatGPT users with your app's backend * the ability to build ChatGPT app widgets that render UX for end users using React, powered by your Gadget app's data ## Quick start  * Create a new Gadget app at [gadget.new](https://gadget.new) * Select **ChatGPT app** type and give your app a name * Follow the in-editor instructions to connect to ChatGPT ## Building MCP servers  Gadget apps can host MCP servers that any MCP client can connect to, including OpenAI's ChatGPT Apps SDK. To setup an MCP server, you can use the `@modelcontextprotocol/sdk` TypeScript package for rich support for the MCP protocol. You can use [HTTP Routes](https://docs.gadget.dev/guides/http-routes) to host the MCP server's endpoints in your Gadget backend. Gadget recommends building MCP servers using the `StreamableHTTP` transport approach, **not** the `SSE` transport approach. OpenAI's ChatGPT Apps SDK has the support for the `StreamableHTTP` transport, and it works best in serverless environments like Gadget. ### Adding an MCP server to your Gadget app  First, add the required NPM packages to your Gadget app: ```bash yarn add @modelcontextprotocol/sdk zod@^3.24.2 ``` Then, create a file that exports a new MCP server instance: ```typescript import { FastifyRequest } from "fastify"; import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import path from "path"; import { readFileSync } from "fs"; // Make a new MCP server instance for each request export const createMCPServer = async (request: FastifyRequest) => { const mcpServer = new McpServer({ name: "example-mcp", version: "1.0.0", }); mcpServer.registerTool( "sayHello", { title: "say hello", description: "print a hello world message", _meta: { "openai/toolInvocation/invoking": "Mentally prepping to say hello", "openai/toolInvocation/invoked": "Hello has been said", }, }, async () => { return { structuredContent: {}, content: [ { type: "text", text: "Well hello there!", }, ], }; } ); return mcpServer; }; ``` Then, set up the two required MCP HTTP routes that implement the MCP protocol. The first route implements the MCP protocol using the `StreamableHTTP` transport. Note that your app's API client is passed into the `createMCPServer` function so you can use it in your MCP tools to have tenancy-aware access to data: ```typescript import { RouteHandler } from "gadget-server"; import { createMCPServer } from "../../mcp"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; /** * Route handler for POST /mcp */ const route: RouteHandler = async ({ request, reply, api, logger, connections }) => { const transport = new StreamableHTTPServerTransport({ // run in the stateless mode that doesn't need persistent sessions sessionIdGenerator: undefined, enableJsonResponse: true, }); const server = await createMCPServer(request); // Take control of the response to prevent Fastify from sending its own response // after the MCP transport has already sent one reply.hijack(); try { await server.connect(transport); await transport.handleRequest(request.raw, reply.raw, request.body); } catch (error) { console.error("Failed to start MCP server session", error); if (!reply.raw.headersSent) { reply.raw.writeHead(500).end("Failed to establish MCP server connection"); } } }; route.options = { cors: { // allow requests to the MCP server from any origin (like ChatGPT) origin: true, }, }; export default route; ``` And the second route completes the implementation of the MCP protocol by handling `GET /mcp` requests using the same transport by re-using the same route handler: ```typescript import { RouteHandler } from "gadget-server"; import Route from "./POST"; // re-use the POST route's implementation of the MCP protocol export default Route; ``` Your app is now hosting an MCP server at `https://--development.gadget.app/mcp`! #### Testing your MCP server  You can test your MCP server by using the MCP Inspector, or using the ChatGPT app in your browser. Gadget recommends using the MCP Inspector to test your MCP server to start, as it provides a more detailed view of the MCP server's capabilities and better error logging. To run the MCP Inspector, you can use the following command: ```bash npx @modelcontextprotocol/inspector --cli https://--development.gadget.app/mcp ``` This will open the MCP Inspector in your browser. You can then use the MCP Inspector to test your MCP server. Make sure you select the `StreamableHTTP` transport in the MCP Inspector, as this is the MCP transport style set up by the above HTTP routes. ### Adding MCP tools  You can add tools to your MCP server by using the `registerTool` method that run server-side JavaScript and return a response to the MCP client. You can read and write data using your Gadget app's `api` object, make calls to external APIs, or anything else you'd like. For example, you can add a tool that returns a random number: ```typescript mcpServer.registerTool( "randomNumber", { title: "random number", description: "return a random number", }, async () => { return { structuredContent: {}, content: [ { type: "text", text: `The random number is ${Math.floor(Math.random() * 100)}`, }, ], }; } ); ``` You can also build responses that read data from your Gadget app's database: ```typescript mcpServer.registerTool( "listTodos", { title: "list todos", description: "list all todos", }, async () => { // use request.api.actAsSession for session-aware data access (for multi-tenant apps) const todos = await request.api.actAsSession.todo.findMany({ first: 10 }); return { structuredContent: { todos: todos.map((todo) => ({ id: todo.id, title: todo.title, })), }, content: [ { type: "text", text: `The todos are ${todos.map((todo) => todo.title).join(", ")}`, }, ], }; } ); ``` #### Use your app's API client in MCP tools  You can use your app's API client in MCP tools to read and write data in a tenancy-aware way. When you use `window.openai.callTool` to make tool calls to your MCP server, OpenAI passes the OAuth token (as `Authentication: Bearer `). This means `request.api.actAsSession` is automatically scoped to the authenticated user, and you can use it to read and write data on behalf of the authenticated user. For example, we can add a tool that creates a new todo item for the authenticated user: ```typescript export const createMCPServer = async (request: FastifyRequest) => { const mcpServer = new McpServer({ name: "chatgpt-todo-list", version: "1.0.0", }); // a session-aware API client for multi-tenant reads and writes const api = request.api.actAsSession; mcpServer.registerTool( "addTodo", { title: "add a todo", description: "add a new todo item to my list", inputSchema: { item: z.string() }, _meta: { "openai/widgetAccessible": true, }, }, async ({ item }) => { // use the api client to create a new todo record const todo = await api.todo.create({ item }); const output = { todo }; return { content: [{ type: "text", text: JSON.stringify(output) }], structuredContent: output, }; } ); }; ``` ### Adding ChatGPT app widgets  ChatGPT apps can render UI widgets in the responses to users, showing them rich UI in addition to plain text responses. You can use Gadget to build these widgets using React code and components in your Gadget app's `web` folder. Gadget will automatically generate widget resource bundles for the ChatGPT Apps SDK to use, complete with hot module reloading and easy bundling for production. A ChatGPT Apps SDK widget is an HTML file, served up to the ChatGPT Apps SDK as an `ui://` resource by your MCP server. You _can't_ serve widgets as normal HTML files over the internet, and instead you must expose them as resources on your MCP server. You must also set up CORS on your MCP server to allow the ChatGPT web sandbox to access your widgets source code cross-origin. #### Installing the required packages  Gadget apps hosting ChatGPT app widgets need to install the `vite-plugin-chatgpt-widgets` and `@gadgetinc/react-chatgpt-apps` packages: ```shell yarn add vite-plugin-chatgpt-widgets @gadgetinc/react-chatgpt-apps ``` Then, add the `vite-plugin-chatgpt-widgets` plugin and set the required `server.cors` properties in your Vite configuration: ```typescript import { defineConfig } from "vite"; import { gadget } from "gadget-server/vite"; import { reactRouter } from "@react-router/dev/vite"; import path from "path"; import { chatGPTWidgetPlugin } from "vite-plugin-chatgpt-widgets"; export default defineConfig({ plugins: [ gadget(), reactRouter(), // mount the chatGPTWidgetPlugin to your Vite config chatGPTWidgetPlugin({ baseUrl: process.env["GADGET_APP_URL"], }), ], resolve: { alias: { "@": path.resolve(__dirname, "./web"), }, }, server: { cors: { // ensure both the Gadget app and the ChatGPT web sandbox can access your widgets source code cross-origin origin: [ process.env["GADGET_APP_URL"]!, "https://web-sandbox.oaiusercontent.com", ], }, }, }); ``` Once the vite plugin is installed, you can add all the built widgets to your MCP server's resources: ```typescript import { getWidgets } from "vite-plugin-chatgpt-widgets"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { FastifyRequest } from "fastify"; import path from "path"; import { getViteHandle } from "gadget-server/vite"; export const createMCPServer = async (request: FastifyRequest) => { const mcpServer = new McpServer({ name: "example-mcp", version: "1.0.0", }); // Get the vite dev server instance from Gadget const devServer = await ( request.server as any ).frontendServerManager?.devServerManager?.getServer(); const viteHandle = await getViteHandle(request.server); // Get the HTML snippet for each widget const widgets = await getWidgets("web/chatgpt", viteHandle); // Register each widget's HTML snippet as a resource for exposure to ChatGPT for (const widget of widgets) { const resourceName = `widget-${widget.name.toLowerCase()}`; const resourceUri = `ui://widget/${widget.name}.html`; mcpServer.registerResource( resourceName, resourceUri, { title: widget.name, description: `ChatGPT widget for ${widget.name}`, }, async () => { return { contents: [ { uri: resourceUri, mimeType: "text/html+skybridge", text: widget.content, }, ], }; } ); } // Set up the required auth tool for authenticating API calls from your client side widgets // don't remove this if you are using the `api` object client side in your widget React code! mcpServer.registerTool( "__getGadgetAuthTokenV1", { title: "Get the gadget auth token", description: "Gets the gadget auth token. Should never be called by LLMs or ChatGPT -- only used for internal auth machinery.", _meta: { // ensure widgets can invoke this tool to get the token "openai/widgetAccessible": true, }, }, async () => { if (!request.headers["authorization"]) { return { structuredContent: { token: null, error: "no token found", }, content: [], }; } const [scheme, token] = request.headers["authorization"].split(" ", 2); if (scheme !== "Bearer") { return { structuredContent: { token: null, error: "incorrect token scheme", }, content: [], }; } return { structuredContent: { token, scheme, }, content: [], }; } ); // set up tools, prompts, etc return mcpServer; }; ``` And then finally, you can add a base template for all your widgets that sets up the right React context for your widgets to render: ```typescript import { Provider as GadgetProvider } from "@gadgetinc/react-chatgpt-apps"; import { api } from "../api"; // import our main app.css file so we inherit all styles that the main frontend is using import "../app.css"; export const ChatGPTWidgetRoot = ({ children }: { children: React.ReactNode }) => { // render within the GadgetProvider context so react hooks like `useFindMany` and `useFetch` work in ChatGPT widgets return {children}; }; export default ChatGPTWidgetRoot; ``` #### Adding a new widget  Once `vite-plugin-chatgpt-widgets` is installed and configured, you can add ChatGPT app widgets to your Gadget app's `web/chatgpt` folder. Each file in this folder will be automatically bundled and made available to the ChatGPT Apps SDK. For example, you can add a widget that displays a "Hello World" message, and then use it in a ChatGPT app's response to a tool call: ```typescript const HelloWorldWidget = () => { return (

Hello World

); }; export default HelloWorldWidget; ``` ```typescript mcpServer.registerTool( "helloWorld", { title: "hello world", description: "display a hello world message", _meta: { // tell ChatGPT to render the web/chatgpt/HelloWorld.tsx widget when this tool is invoked "openai/outputTemplate": "ui://widget/HelloWorld.html", }, }, async () => { return { structuredContent: {}, content: [ { type: "text", text: "Hello World", }, ], }; } ); ``` #### Fetching backend data  Widgets can fetch backend data using the normal React hooks from `@gadgetinc/react`. For example, you can fetch a list of todos from your Gadget API in a widget using the [`useFindMany` hook](https://docs.gadget.dev/reference/react#usefindmany): ```tsx import { useFindMany } from "@gadgetinc/react"; import { api } from "../api"; const TodoList = () => { // use the useFindMany hook to fetch the todos via the app's GraphQL API instead of an MCP tool call const [{ data, fetching, error }, refresh] = useFindMany(api.todo); return (
    {data.map((todo) => (
  • {todo.title}
  • ))}
); }; export default TodoList; ``` Or you can add a form for creating a new todo using the [`useAction` hook](https://docs.gadget.dev/reference/react#useaction): ```tsx import { useAction } from "@gadgetinc/react"; import { api } from "../api"; const CreateTodoForm = () => { // setup an action we can call when the form submits const [{ data, fetching, error }, createTodo] = useAction(api.todo.create); const [title, setTitle] = useState(""); return (
createTodo({ title })}> setTitle(e.target.value)} />
); }; export default CreateTodoForm; ``` Read more about the React hooks from `@gadgetinc/react` in the [frontend guide](https://docs.gadget.dev/guides/frontend/building-frontends). #### Widget authentication  When a user installs your ChatGPT app, they will authenticate themselves with your Gadget backend, and establish an identity in your backend `session` or `user` model. When you make API calls to your Gadget API, or use MCP server tools, Gadget will activate the ChatGPT user's identity for those calls. That means that your Gadget app's normal access control checks will apply to reads and writes made by the ChatGPT user from within widget. Read more about access control in the [Access Control](https://docs.gadget.dev/guides/access-control) guide. ### Using without authentication  If you don't need authentication for the API calls made by your ChatGPT widgets such that they are safe to be world readable, you can disable authentication in the provider by setting the `authenticate` prop to false: ```typescript import { Provider } from "@gadgetinc/react-chatgpt-apps"; import { api } from "../api"; function App() { return ( ); } export default App; ``` ### React hooks for the `window.openai` API  The `window.openai` API is the bridge between you frontend and ChatGPT. It provides data and functions you can use in your app to do things like: * hydrate your widget's initial state * save a widgets state so it is preserved between refreshes * make additional tool calls * handle different display modes Gadget provides hooks you can use to interact with `window.openai`. Documentation for these hooks can be found in the [`@gadgetinc/react-chatgpt-apps` reference](https://docs.gadget.dev/reference/react-chatgpt-apps). More information about `window.openai` can be found in the [Apps SDK docs](https://developers.openai.com/apps-sdk/build/custom-ux#understand-the-windowopenai-api). ## Built-in OAuth authentication  When you add the ChatGPT Apps connection to your Gadget app, your app automatically becomes an **OIDC-compliant authorization server** powered by the [`node-oidc-provider` library](https://github.com/panva/node-oidc-provider). This means ChatGPT users can securely authenticate with your backend, establishing an identity that can be used for access control and personalization. The only configuration required is specifying which route users will be directed to when granting consent. By default, this is the `/authorize` route, which is powered by the `web/routes/authorize.tsx` file that Gadget generates for you when you add the connection. For more details on how to customize the OAuth flow and integrate it with your app's authentication system, see the section below. ## Gadget as an OAuth Provider  Gadget automatically acts as a **spec-compliant OAuth 2.1 / OIDC authorization server** when your app has an active ChatGPT connection. This means your app can authenticate users from external services like ChatGPT without you having to implement the OAuth protocol yourself. ### How it works  The OAuth flow in Gadget follows the standard OAuth 2.1 protocol with PKCE (Proof Key for Code Exchange): 1. A user attempts to install or connect your ChatGPT app 2. ChatGPT initiates an OAuth flow by redirecting the user to your Gadget app's authorization endpoint 3. Gadget redirects the user to your configured authorization route (by default `/authorize`) 4. Your app presents a consent screen and authenticates the user 5. Once the user grants consent, your app posts to Gadget's OAuth endpoint 6. Gadget runs any configured install actions on your session and user models 7. If successful, Gadget completes the OAuth flow and issues tokens to ChatGPT 8. ChatGPT can now make authenticated requests to your app on behalf of the user ```mermaid sequenceDiagram participant User participant ChatGPT participant Gadget participant App User->>ChatGPT: 1. Attempts to install or connect ChatGPT app ChatGPT->>Gadget: 2. Redirects user to Gadget's OAuth authorization endpoint Gadget->>App: 3. Redirects user to app's /authorize route App->>User: 4. Displays consent screen and authenticates user User->>App: Grants consent App->>Gadget: 5. Posts authorization result to Gadget's OAuth endpoint Gadget->>Gadget: 6. Runs install actions on session and user models Gadget->>ChatGPT: 7. Completes OAuth flow and issues tokens ChatGPT->>App: 8. Makes authenticated requests on behalf of user ``` ### Configuring the authorization route  You need to specify a route where users will be taken to consent to the OAuth grant. This is configured in the ChatGPT connection settings in the Gadget editor. The authorization route can be: * **A frontend route** (recommended) - defined using React Router's [framework mode](https://docs.gadget.dev/guides/frontend/react-router-in-gadget), which provides a better user experience with full access to React components and hooks * **A backend route** - if you prefer to render HTML directly from your backend When a user is directed to your authorization route, the following query parameters will be present: | Parameter | Description | | --- | --- | | `code` | A short-lived code needed to complete the OAuth flow. **Note:** This is NOT the OAuth authorization code (that gets issued automatically by Gadget). | | `prompt` | Either `"consent"` or `"none"` - indicates whether user interaction is required. | | `scope` | The scopes that the client is requesting access to. | ### Implementing the authorization route  Your authorization route is responsible for: 1. **Authenticating the user** - determining who is granting access 2. **Establishing a session** - creating or updating the session that will be associated with the OAuth grant 3. **Presenting consent UI** - showing the user what they're granting access to 4. **Posting to the authorization endpoint** - completing the OAuth flow Here's the default `web/routes/authorize.tsx` file that Gadget generates for you: ```tsx import { api } from "../api"; import { redirect } from "react-router"; import type { Route } from "./+types/authorize"; export const loader = async ({ context, request }: Route.LoaderArgs) => { const url = new URL(request.url); const code = url.searchParams.get("code"); if (!code) { throw new Error("Missing required OAuth parameter: code"); } // If you have a user model, you can check if the user is logged in const { session, gadgetConfig } = context; const userId = session?.get("user"); const user = userId ? await context.api.user.findOne(userId) : undefined; // If no user is logged in, redirect to sign in if (!user) { const redirectTo = `${url.pathname}${url.search}`; return redirect(`${gadgetConfig.authentication!.signInPath}?redirectTo=${encodeURIComponent(redirectTo)}`); } return { csrfToken: session?.get("csrfToken"), user, code, }; }; export default function Authorize({ loaderData }: Route.ComponentProps) { const { csrfToken, user, code } = loaderData; return (

Authorize Access

ChatGPT would like to access your account.

{/* Form that posts to the OAuth endpoint */}
{user.email &&

Logged in as {user.email}

}
); } ``` ### Working without a user model  Not all apps need a dedicated user model. Some apps may want to authorize access on a per-session basis, allowing each ChatGPT session to have its own isolated data without requiring user accounts. In this case, you can skip the authentication check and simply use the session that Gadget automatically provides: ```tsx export const loader = async ({ context, request }: Route.LoaderArgs) => { const url = new URL(request.url); const code = url.searchParams.get("code"); if (!code) { throw new Error("Missing required OAuth parameter: code"); } // Just use the session directly, no user required return { csrfToken: context.session?.get("csrfToken"), code, }; }; export default function Authorize({ loaderData }: Route.ComponentProps) { const { csrfToken, code } = loaderData; return (

Authorize Access

ChatGPT would like to access this app.

); } ``` ### Completing the OAuth flow  Once you've authenticated the user and they've granted consent, you need to POST to: ```markdown /api/oauth/v2/interaction/${code}/authorize ``` Where `code` is the parameter from the query string. This tells Gadget to proceed with the OAuth flow. Make sure to include the CSRF token in your POST request to prevent cross-site request forgery attacks. The CSRF token is available in the session as `csrfToken`. ### Authentication tokens and session management  After the OAuth code exchange is complete, Gadget issues an **authentication token** that ChatGPT will use for all subsequent requests to your app. This token is a **symmetrically signed JWT** that contains the session information established during the authorization flow. ChatGPT includes this token as a **Bearer token** in the `Authorization` header for all requests (made via tool calls) to your MCP server: ```markdown Authorization: Bearer ``` When Gadget receives a request with this token, it automatically: 1. **Restores the session** - Sets the request's session to be the same session that was established during the initial OAuth flow 2. **Activates the user context** - If a user was signed in during authorization, all ChatGPT requests to your MCP server will have that user as the authenticated tenant This means that when you use `request.api.actAsSession` in your MCP tools (as shown in the ), the API client is automatically scoped to the authenticated user who authorized the connection. Your app's access control rules and tenant isolation will work exactly as expected, with each ChatGPT user only able to access their own data. This token is also used to make requests from your widget using your app's API client, thanks to the `Provider` set up in `web/chatgpt/root.tsx`. See the [`@gadgetinc/react-chatgpt-app` reference](https://docs.gadget.dev/reference/react-chatgpt-apps) for more details. For example, if a user with ID `"123"` authorizes your ChatGPT app, all subsequent MCP tool calls will execute with that user's session and identity: ```typescript mcpServer.registerTool( "getTodos", { title: "get user's todos", description: "fetch todo data for the authenticated user", }, async () => { // This API call is automatically scoped to the authenticated user // who authorized the ChatGPT app - no need to manually pass user IDs! // the todos will only be those that belong to the authenticated user const todos = await request.api.actAsSession.todo.findMany(); return { structuredContent: { data }, content: [{ type: "text", text: JSON.stringify(todos) }], }; } ); ``` The JWT token is symmetrically signed, meaning Gadget uses a shared secret to both create and verify the token. This ensures that only your Gadget app can issue and validate tokens for your OAuth implementation. ### Running actions on install  Before completing the OAuth flow, Gadget will look for any actions on your `session` model (and `user` model if it exists) that have the **ChatGPT App Install** trigger configured. These actions will be called automatically, allowing you to: * Set up initial data for the user * Record analytics about the installation * Validate that the user is allowed to connect * Perform any other custom logic By default, no actions have this trigger configured. You need to add it manually if you want to run custom code. To add the ChatGPT App Install trigger to an action: 1. Navigate to the action you want to configure (e.g., `session/update` or `user/update`) 2. Click on the **Triggers** tab 3. Add the **ChatGPT App Install** trigger The action will receive a payload with information about the OAuth request: ```typescript export const onSuccess: ActionOnSuccess = async ({ params, record, trigger }) => { if (trigger?.type === "chatgpt_app_install") { // Access OAuth information from the trigger const { oauth, userId, sessionId } = trigger; // oauth.scope - the scopes being granted // oauth.clientId - the OAuth client ID // userId - the user ID (if user model exists) // sessionId - the session ID // Perform any setup needed for this installation await api.yourModel.create({ session: { _link: sessionId }, // ... other fields }); } }; ``` If any of your install actions throw an error, the entire OAuth flow will be aborted and the user will not be able to connect. Make sure your actions handle errors gracefully! ### Dynamic client registration  Gadget's OAuth implementation supports **dynamic client registration** as specified in the OAuth 2.0 Dynamic Client Registration Protocol ([RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591)). This means OAuth clients like ChatGPT can register themselves automatically without requiring manual configuration in your Gadget app. When a new client attempts to connect, Gadget will automatically register it and allow the OAuth flow to proceed. This makes it easy to connect multiple OAuth clients to your app without additional setup. ### OAuth endpoints  Gadget exposes the following OAuth endpoints for your app: | Endpoint | Description | | --- | --- | | `/api/oauth/v2/auth` | OAuth authorization endpoint - where OAuth clients begin the flow | | `/api/oauth/v2/token` | Token endpoint - where clients exchange authorization codes for access tokens | | `/api/oauth/v2/reg` | Dynamic Client Registration endpoint - where OAuth clients can register themselves automatically | | `/api/oauth/v2/interaction/:code` | Interaction details - used internally by the authorization flow | | `/api/oauth/v2/interaction/:code/authorize` | Authorization completion - where your app posts to complete the flow | | `/.well-known/openid-configuration` | OpenID Connect discovery document - provides OIDC configuration metadata | | `/.well-known/oauth-protected-resource` | OAuth 2.0 Protected Resource Metadata - provides information about the protected resource endpoints | | `/.well-known/oauth-authorization-server` | OAuth 2.0 Authorization Server Metadata - provides OAuth server configuration and capabilities | These endpoints are fully managed by Gadget and implement the OAuth 2.1 and OIDC specifications. ### Customizing the OAuth configuration  The OAuth server configuration is managed automatically by Gadget based on your ChatGPT connection settings. The main customization you need to make is specifying the authorization route where users grant consent. You can update this in the Gadget editor: 1. Navigate to **Settings** → **Connections** → **ChatGPT** 2. Update the **Authorization Path** field 3. Deploy your changes The authorization path must be a route in your Gadget app. It can be either a .