Connect ChatGPT and Claude
Create an OAuth client for each host, enter the right settings, test with MCP Inspector, and fix common connection failures.
This page connects ChatGPT and Claude to an MCP server built with Add MCP to your application. Each host gets its own OAuth client. A person connects, signs in to your application, and allows access on your consent page. The host then calls your tools with a ten-minute access token and renews it while the person's session lasts.
Before you connect
- Deploy the application and Convex with HTTPS origins. Hosts do not connect to a loopback address.
- Set
SITE_URLto the exact public Nuxt origin. The OAuth issuer is${SITE_URL}/api/auth. - Run
auth:ensureSigningKeyand check that its key ID appears athttps://your-app.example/api/auth/jwks. - List each host in the profile:
oauth: { mcp: { scopes, hosts: ['chatgpt', 'claude'] } }. - Export an internal mutation that calls
auth.oauthOperator.createHostClient, as in the recipe'sconvex/connections.ts. - Test the flow with MCP Inspector first.
What each host needs
| Setting | ChatGPT | Claude |
|---|---|---|
| MCP server URL | https://<deployment>.convex.site/mcp | the same URL |
| Authentication | OAuth | OAuth |
| Client registration | User-defined OAuth client | Your own OAuth client |
| Client ID | From createHostClient | From createHostClient |
| Client secret | Empty | Empty |
| Token endpoint auth | none | Not asked |
| Callback URL | Shown by ChatGPT; pass it to createHostClient | Fixed: https://claude.ai/api/mcp/auth_callback |
| Scopes | Select your scopes; add offline_access to base scopes for renewal | Requested by Claude from your metadata |
The clients are public clients with S256 PKCE. They have no secret. Dynamic client registration is off, so the host cannot create a client itself.
Connect Claude
Create the client:
bash pnpm exec better-convex convex run connections:createHostClient '{"host":"claude"}'Record the returned
clientId. The client accepts only Claude's fixed callback and every scope in your profile.- In Claude, add a custom connector. Enter the MCP server URL.
- Select Use your own OAuth client and paste the client ID. Leave the secret empty.
- Connect. Claude opens your login page, then your consent page. Sign in and select Allow.
- Start a new conversation and ask for something one of your tools can do.
Claude reads scopes_supported from your protected-resource metadata. It
can request read access and renewal first, and ask for write consent the first
time a write tool needs it.
Connect ChatGPT
ChatGPT shows its callback URL during setup, so create the client in the middle of the setup.
- In ChatGPT, start a new connector in the developer connection settings. Enter the MCP server URL and select OAuth.
- Copy the exact callback URL that ChatGPT displays. It is
https://chatgpt.com/connector_platform_oauth_redirectorhttps://chatgpt.com/connector/oauth/<callback-id>. Do not guess the callback ID. Create the client with that callback:
bash pnpm exec better-convex convex run connections:createHostClient \ '{"host":"chatgpt","redirectUri":"https://chatgpt.com/connector_platform_oauth_redirect"}'- In Advanced OAuth settings, set Registration method to User-Defined OAuth Client and Token endpoint auth method to none. Paste the client ID and leave the client secret empty.
- Under Default scopes, select the scopes the connection needs. Enter
offline_accessin Base scopes so that ChatGPT receives a refresh token. Without it, ChatGPT asks the person to reconnect after ten minutes. - Connect, sign in, and allow access. Refresh the connector's tools, then start a new conversation.
Scopes and consent
Your consent page shows the verified client name, the exact resource, and each
requested scope with its description from oauth.mcp.scopes. Consent grants
exactly the requested scopes, never more.
- A tool with
scopes: ['notes:write']is visible to every connection. A connection without that scope receives an HTTP 403insufficient_scopechallenge when it calls the tool. The host can then ask the person for consent again. offline_accessallows renewal. The refresh token ends with the Better Auth session that granted consent, and after at most seven days. Signing out of your application ends renewal.- A broader permission always needs new consent. An earlier grant is never widened silently.
Each connection is one row on the person's connections page
(auth.oauthConnections.list). Disconnect there revokes the consent and
the refresh tokens on the server. The host's next request fails.
Disconnecting inside the host does not always revoke the grant on your server.
Revoke it on your connections page. To stop a host for every user, disable its
client with auth.oauthOperator.setClientDisabled.
Test with MCP Inspector
Test the OAuth flow and your tools locally before you connect a real host.
Create a client for Inspector's local callback. Run this internal mutation only in development:
convex/connections.ts export const createInspectorClient = internalMutation({ args: {}, handler: async (ctx) => await auth.oauthOperator.createPublicClient(ctx, { name: 'MCP Inspector', profile: 'mcp-inspector', redirectUris: ['http://localhost:6274/oauth/callback'], resource: { identifier: auth.mcp.resource().href, name: 'MCP', ownership: 'application' }, scopes: ['notes:read'], }), })Use the same resource
nameascreateHostClient: yourappName, orMCPwhen you have not set one.- Run
pnpm dlx @modelcontextprotocol/inspector. - Select the Streamable HTTP transport and enter your
CONVEX_SITE_URLfollowed by/mcp. - In the OAuth settings, enter the client ID, leave the secret empty, and request your scopes.
- Connect, sign in, and allow access. List the tools and call one.
- Disconnect on your connections page, then call the tool again. The call must fail with HTTP 401.
For automated checks, snapshot the catalog with listMcpCatalog as shown in
MCP on Convex.
Update tools after a deploy
Hosts cache the tool list. After you change tools:
- In ChatGPT, refresh the connector's tools, then start a new conversation.
- In Claude, start a new conversation. Claude can keep an older catalog for a while.
- Keep MCP Apps
ui://URIs stable. See MCP Apps.
Common failures
| Symptom | Cause and fix |
|---|---|
createHostClient fails with AUTH_OAUTH_CLIENT_HOST_NOT_ENABLED | Add the host to oauth.mcp.hosts. |
createHostClient fails with AUTH_OAUTH_CLIENT_REDIRECT_URI_INVALID | Pass the exact ChatGPT callback. For Claude, omit redirectUri. |
The host shows a redirect or invalid_client error | The client's callback does not match the host's callback, or the client ID is wrong. Create a new client with the exact callback. |
| The login page says the request is invalid or expired | The signed authorization request is older than two minutes, or the page changed its query. Start the connection again from the host. |
Every tool call returns HTTP 401 invalid_token | SITE_URL does not match the issuer in the token, the resource URL differs from CONVEX_SITE_URL + /mcp, no signing key exists, or the grant was revoked. |
A tool call returns HTTP 403 insufficient_scope | The connection lacks the tool's scope. Allow the new scope when the host asks. Check that the client was created with that scope. |
| The host asks to reconnect every ten minutes | The connection has no offline_access. In ChatGPT, add it to base scopes. Check that renewal is not false. A session that ended also ends renewal. |
A tool result says MCP_ACCESS_DENIED | The grant, session, or client changed after the token was issued. Reconnect. |
HTTP 400 with JSON-RPC -32022 or -32020 | The client does not send the MCP-Protocol-Version: 2026-07-28 header. Only the modern protocol is supported. |
| New tools do not appear | Refresh the host's tool list and start a new conversation. |