Consumer OAuth for Custom MCP Servers
How per-user (Consumer OAuth) authentication works in Chipp, which connectors support it, and how a custom MCP server or custom action can act with a user's own Google Drive connection

Consumer OAuth lets each end-user of your Chipp app sign in with their own account on a third-party service. Instead of one shared API key, each user connects a personal account (for example their Google Drive), and the AI agent acts on their behalf.
Custom MCP servers do not support Consumer OAuth. The Connect Custom Server dialog has no “Consumer OAuth (per-user)” option and no Authorize URL, Token URL, Client ID, or Client Secret fields. An earlier version of this guide described those controls. They were never part of the custom server dialog. To let your own server act in a user’s Google Drive, use Delegated user connection (Google Drive) (see below).
Authentication Options for Custom MCP Servers
To add a custom MCP server, open your app, click Brain, and open the Integrations tab. Click Browse all integrations, then click the Custom Server card. In the Connect Custom Server dialog, choose one of these:
| Option | Use it when |
|---|---|
| None | The MCP server is public and needs no credentials |
| Bearer Token / API Key | The server expects Authorization: Bearer <token> and one shared token works for every user |
| API Key (Custom Header) | The server expects its key in a header you name (for example X-API-Key) |
| Delegated user connection (Google Drive) | Your server must act in the invoking user’s own Google Drive. Chipp sends a short-lived delegation token that your server redeems. |
The first three send one shared credential for every user of your app. They do not identify the individual end-user to the third-party service. The fourth acts for the individual user. The custom action form has the same Delegated user connection (Google Drive) option in its authentication select. For setup details and server-side verification, see Building Custom MCP Servers.
When You Need Per-User Accounts
Use per-user authentication when:
- Users need to access their own data (files, calendar events, emails)
- Actions must be attributed to the specific user
- The service enforces per-user rate limits or permissions
Use a shared credential (Bearer Token / API Key) when:
- You access shared company resources that all users can see
- The MCP server connects to internal APIs you control
- You want the simplest possible setup
If you need per-user access, use one of the supported options below.
Supported Per-User Options
Hosted per-user integrations
Chipp’s hosted integrations let each end-user connect their own account inside Chipp. Chipp stores the tokens per user, refreshes them, and runs the tools for you. Your builder setup does not need an OAuth app, and no token is sent to a custom server. See the integration guides, for example:
- Google Drive - search, browse and read the connected user’s files, and save files to their Drive
- Google Calendar
- Gmail
Save files to the user’s Google Drive
With per-user Google Drive, your agent can save a generated file (such as a PDF) into the connected user’s own Drive with the google_drive_save_file tool. The user connects Drive once inside Chipp. Chipp uploads the file with that user’s credential. The token never reaches the model, a custom action, or an external server.
- The tool needs the optional Drive permission to create files. Users who connected Drive before this permission existed keep working for reads. When they ask to save a file, Chipp prompts them to reconnect and allow saving files.
- Folder restrictions you configure still apply.
- Each user’s file is written only through that user’s own connection.
If your external service generates the file, you have two choices. Return the file to Chipp (a file reference, an HTTPS URL, or base64 content) and let google_drive_save_file write it. Or let your service upload the file itself with a delegated user connection, so the file never passes through Chipp. Either way, users never run a second OAuth flow.
Delegated user connection (Google Drive)
Use this when your own service needs to do something in an end-user’s Google Drive, for example generate a PDF and save it to the user’s Drive folder, without a second Google sign-in and without sending the file back through Chipp.
How it works
- The end-user connects Google Drive once, inside Chipp.
- When the AI calls your action or MCP tool, Chipp finds that user’s Drive connection and checks it allows saving files.
- Chipp signs a short-lived delegation token and sends it to your server in a request header.
- Your server exchanges the token with Chipp. At that moment Chipp asks Google for a new access token limited to
drive.fileonly, and returns it. - Your server calls the Google Drive API directly. The file never passes through Chipp.
Chipp never forwards the user’s stored Google token, and never exposes a refresh token. The model and the conversation never see any of these tokens. The drive.file token is not saved anywhere in Chipp.
Before you start: turn on per-user Google Drive
The hosted Google Drive integration must be enabled on your app, with Per-user accounts switched on, so that each end-user can connect their own Drive. Open the Google Drive integration for your app and turn on Per-user accounts. Without it, the Connect Google Drive and Reconnect Google Drive prompts in chat fail, and delegated calls return DELEGATION_CONNECTION_REQUIRED.
Configure it
- Choose Delegated user connection (Google Drive) as the authentication option.
- Set the fields:
| Field | Value |
|---|---|
| Provider connection | Google Drive (fixed) |
| Required permission | drive.file (fixed) |
| Approved destination host | The one host allowed to receive the token. It is prefilled from your URL and must equal it. |
| Header name | Defaults to X-Chipp-Delegation-Token. |
Header names that carry other credentials or control the connection (for example Authorization, Cookie, Host, Content-Type) are rejected with a 400 error. Use the default unless you need another name.
You can read the saved settings back in the builder. They contain only those four fields. No token is ever stored or shown.
The destination allowlist
The token is sent only when the request URL’s host exactly matches the destination host.
- Match is exact.
pdf.example.comdoes not allowevil.pdf.example.com,pdf.example.com.evil.net, or a different port. - Destinations must be a DNS host name over
https. IP addresses, wildcards, paths and credentials in the host are rejected. - If the action URL (after variables are filled in) points to any other host, Chipp sends nothing and returns
DELEGATION_DESTINATION_MISMATCH. - Chipp does not follow redirects on a delegated call. Any
3xxresponse is treated as a destination mismatch and no second request is made. Point the action directly at the final URL. - For an MCP server, the destination host must equal the MCP server URL’s host, and the token is sent with tool calls over the HTTP transport. The SSE transport cannot carry it, so the call fails instead of running without it.
The delegation token
| Property | Value |
|---|---|
| Format | Signed JWT (HS256) |
| Lifetime | 5 minutes at most |
| Use | Single use. A second redemption fails. |
| Audience | The destination origin, https://<destination host> |
| Bound to | The invoking user, your app, the provider and the scope |
A new token is created for every call. If Chipp has to send a call again (for example after a rate-limit retry), it creates another new token. A used token is never sent twice. The claims are iss, aud, dh (destination host), sub (the user), aid (your app), prv (provider), scp (scope), jti, iat and exp.
Treat the token as a secret. Do not log it. If your server echoes it in a response, Chipp replaces it with [REDACTED] before the model sees the result.
Redeem the token
Your server calls the Chipp exchange endpoint:
POST https://build.chipp.ai/api/delegation/v1/exchange
Content-Type: application/json
{
"token": "<value of the X-Chipp-Delegation-Token header>",
"audience": "https://pdf.example.com",
"destinationHost": "pdf.example.com"
}audience and destinationHost must match the token’s aud and dh. A successful reply has Cache-Control: no-store and exactly these fields:
{
"access_token": "ya29...",
"token_type": "Bearer",
"expires_in": 3599,
"scope": "https://www.googleapis.com/auth/drive.file"
}scope is exactly https://www.googleapis.com/auth/drive.file. Chipp checks Google’s answer and refuses the exchange (DELEGATION_FAILED) if the token carries any other scope. expires_in is Google’s own lifetime for the new token. There is no refresh_token. Use the access token before expires_in runs out. For another file, wait for the next call from Chipp, which sends a new token.
Example service
The Chipp repository includes a runnable example: examples/delegated-drive-pdf-service. It exposes POST /generate-pdf with { "folderId", "title", "content" }, reads X-Chipp-Delegation-Token, redeems it at the exchange endpoint, generates a PDF, uploads it with the Drive multipart upload API (POST https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart), and returns only the file’s id, name and link. It never returns the PDF bytes to Chipp and never logs the token. Its README has setup steps and a sample action definition.
Under the drive.file scope Google only lets an app touch files it created or that the user opened with it. Whether an upload into a folder the user picked is accepted has not been confirmed end to end. If Google refuses the folder, upload to the user’s My Drive root instead and tell the user.
The user’s side: connect and reconnect
The end-user stays in Chipp the whole time.
- First time: if the user has not connected Google Drive, the action does not run. The chat shows a Connect Google Drive prompt.
- Read-only connections: users who connected Drive before this feature only granted read access. The chat shows a Reconnect Google Drive prompt that asks them to approve saving files (
drive.file) in addition to read access. It updates the same Google Drive connection. It does not create a second one, and the existing Drive read tools keep working. - Revoked or expired: if the user removed access in their Google account, or Google refuses to issue a new token for the grant, the chat shows the same reconnect prompt.
Delegation needs a signed-in end-user. Anonymous visitors and voice sessions cannot use it.
Error codes
| Code | Meaning | What to do | Exchange HTTP status |
|---|---|---|---|
DELEGATION_CONSUMER_IDENTITY_REQUIRED | No signed-in user for this session (anonymous, voice, or a builder test) | Ask the user to sign in | 403 |
DELEGATION_CONNECTION_REQUIRED | The user has not connected Google Drive | User connects Google Drive in chat | 409 |
DELEGATION_INSUFFICIENT_SCOPE | The connection lacks drive.file | User reconnects and approves file access | 403 |
DELEGATION_GRANT_REVOKED | The Google grant expired or was revoked (Google answered invalid_grant) | User reconnects Google Drive | 409 |
DELEGATION_DESTINATION_MISMATCH | The URL host is not the approved host, a redirect was returned, the audience or host you sent differs from the token’s, or the approved host was changed | Fix the action URL or the destination host. Do not retry. | 403 |
DELEGATION_FAILED | Bad signature, expired token, a token used twice, Google returned a token with scopes other than drive.file, or an internal failure | Try again. If it persists, check the delegation settings. | 401 |
In chat, a failed action shows the code in square brackets, for example [DELEGATION_CONNECTION_REQUIRED], with a plain-language message for the user. Exchange errors are returned as { "error": { "code": "...", "message": "..." } }.
Isolation
Each token is redeemed against the (user, app, Google Drive) connection it was minted for and nothing else. A token for one user can never return another user’s Google token, and altering the token invalidates its signature. Delegation is not available as a template variable: there is no {{...}} placeholder that exposes a user’s token.
Catalog connectors with a Per-user toggle
Some connectors in the integrations catalog (for example Meta Ads and Canva) offer a per-user path. To use it:
- Open your app, click Brain, and open the Integrations tab. Click Browse all integrations and select the catalog connector.
- Turn on the Per-user accounts toggle. Each end-user then authorizes with their own account.
- Click Save & Enable. This creates the integration.
- Open the connector again. Click Test: connect your own account to run the sign-in flow as a user and check that it works. This button appears only after the integration exists and the toggle is on.
The Per-user accounts toggle and Test: connect your own account appear only on supported catalog connectors. They do not appear in the Connect Custom Server dialog. These connectors are built into Chipp. They are not custom MCP servers, and Chipp manages their OAuth settings.
How Per-User Authentication Works
Consumer OAuth Flow
On catalog connectors with the Per-user accounts toggle, each end-user authorizes with their own account on the third-party service. Chipp stores the tokens and refreshes them. Custom MCP servers do not offer this flow.
- User chats with your Chipp app
- The AI agent calls a tool that needs the user’s account
- Chipp detects the user has not connected that account
- The user sees a connect prompt and clicks it
- The provider’s consent page opens and the user authorizes access
- The callback returns an authorization code to Chipp
- Chipp exchanges the code for access and refresh tokens
- Tokens are stored securely per user
- The tool runs with that user’s access token
Later requests reuse the stored tokens.
Callback URLs (reference)
The per-user flow redirects to fixed, Chipp-hosted callback URLs. They are the same for every Chipp app. Do not replace dino-mullet.chipp.ai with your own app’s domain. Register them only if a connector’s setup instructions ask you to register an OAuth app, and copy them exactly.
Per-user MCP connectors use these two URLs. Each one is a path on a Chipp-hosted origin:
| Setting | Value | Origin |
|---|---|---|
| Per-user callback (runtime) | https://dino-mullet.chipp.ai/consumer/mcp/oauth/callback | APP_URL (the Chipp API origin) |
| Builder MCP connection callback | https://build.chipp.ai/api/integrations/mcp/oauth/callback | WEB_APP_URL (the builder app origin) |
Hosted integrations (such as Google Drive) use different paths on the same API origin (APP_URL). Which one applies depends on whose OAuth client signs the user in:
| Credentials | Per-user sign-in callback | Builder sign-in callback |
|---|---|---|
| Chipp platform credentials (Chipp’s own OAuth client) | https://dino-mullet.chipp.ai/api/integrations/hosted/oauth/callback | https://dino-mullet.chipp.ai/api/integrations/hosted/oauth/callback |
| Your own OAuth client (credentials you add under Advanced) | https://dino-mullet.chipp.ai/consumer/oauth/callback | https://dino-mullet.chipp.ai/api/integrations/hosted/oauth/callback |
With platform credentials, Chipp has already registered the callback, so you register nothing. With your own OAuth client, register both URLs in your provider’s allowed-callback list, exactly as written. The connector’s Advanced panel shows the same values.
Providers that accept one callback URL (iWave)
A few providers give each partner an OAuth client with exactly one allowed redirect URI. iWave (Kindsight) is the one today. For these providers Chipp sends the same hosted callback for both per-user and builder sign-in, even when you use your own OAuth client:
| Credentials | Per-user sign-in callback | Builder sign-in callback |
|---|---|---|
| Chipp platform credentials or your own OAuth client | https://dino-mullet.chipp.ai/api/integrations/hosted/oauth/callback | https://dino-mullet.chipp.ai/api/integrations/hosted/oauth/callback |
Register only that one URL. Do not register https://dino-mullet.chipp.ai/consumer/oauth/callback for these providers: the provider cannot hold it, and Chipp never sends it. The connector’s Advanced panel shows the same single URL in both fields.
Token Requests
When Chipp exchanges or refreshes a per-user token, it sends a standard OAuth 2.0 request to the connector’s token URL:
- PKCE: Chipp always sends
code_challenge_method=S256in the authorization request. It sends the matchingcode_verifierwhen it exchanges the code. - Grants: The request uses
grant_type=authorization_codefor the first exchange andgrant_type=refresh_tokenfor a refresh. - Body format: The body is
application/x-www-form-urlencoded. - Client authentication: The connector uses one of three styles:
form_post- Chipp sendsclient_idandclient_secretin the body.form_basic- Chipp sends the client ID and secret in an HTTP BasicAuthorizationheader, and also sendsclient_idin the body.json_basic- Chipp sends the client ID and secret in an HTTP BasicAuthorizationheader only. The body has noclient_id.
- Public clients: A public client has no client secret. Chipp sends
client_idonly, with noAuthorizationheader. - Optional parameters: If the connector sets a resource indicator (RFC 8707), Chipp adds
resourceto the authorization, exchange and refresh requests. Some connectors (for example Meta) use aconfig_idin place ofscopein the authorization request.
Token Refresh
Access tokens expire. Chipp refreshes them automatically:
- Proactive refresh: Chipp resolves the token once per chat turn, when it registers the connector’s tools at the start of the turn. At that time, Chipp checks if the token expires within 5 minutes (a 5-minute buffer). If it does and a refresh token exists, Chipp requests a new access token and stores it. Every call in that turn uses the token from that step.
- Reactive refresh: If the connector’s server answers a call with
401, Chipp refreshes the token and retries the call one time. - Permanent refresh error: If the provider rejects the refresh for good (for example
invalid_grant, because the user revoked access), the user is prompted to reconnect. - Transient refresh error: If the refresh fails for a temporary reason (a network error or a
5xxresponse), Chipp falls back to the stored token. That token is stale but not yet expired. A later401triggers the reactive refresh.
Some providers (like Google) only return a refresh token on the first authorization. If a user must reconnect, they may need to revoke access in their account settings first.
User Experience
- First time: the user sees a prompt to connect their account
- Click to connect: the provider’s login and consent page opens
- Authorize: the user grants access
- Automatic return: the window closes and the chat continues
- Later chats: no prompts, the stored tokens are reused
How Chipp Delivers the Token to the MCP Server
For a per-user MCP connector, Chipp sends the user’s access token to that connector’s MCP server on each request. The token is in the Authorization request header, in the Bearer format:
Authorization: Bearer <user_access_token><user_access_token> is a placeholder. A server reads the token from the Authorization request header of each incoming MCP request.
This applies to per-user catalog connectors only. A custom MCP server does not receive a per-user token in Authorization. It receives the shared credential you set in the modal, or, with a delegated user connection, a delegation token it redeems for a drive.file token (see Authentication Options for Custom MCP Servers).
Example handler for a shared secret
If you build a custom MCP server that uses Bearer Token / API Key, your server gets the same shared secret on every request. This handler checks it:
import { timingSafeEqual } from "node:crypto";
const EXPECTED_SECRET = process.env.MCP_SHARED_SECRET ?? "";
export function authorize(req: Request): Response | null {
const header = req.headers.get("authorization") ?? "";
const match = /^Bearer (.+)$/.exec(header);
if (!match) {
return new Response("Unauthorized", { status: 401 });
}
const given = Buffer.from(match[1]);
const expected = Buffer.from(EXPECTED_SECRET);
if (given.length !== expected.length || !timingSafeEqual(given, expected)) {
return new Response("Unauthorized", { status: 401 });
}
return null; // authorized: continue to the MCP handler
}The handler rejects a missing or malformed header with 401. It compares the token to your secret in constant time. The token here is your own shared secret. It is not a per-user token issued by Chipp.
Example handler for a per-user Google token
Use this pattern only for an MCP server that Chipp calls with a per-user Google access token in the Authorization header. The Connect Custom Server dialog does not send such a token. For a custom server, redeem a delegation token instead. The handler shows the general shape. The token is read from the header of the incoming request and used for one Google Drive upload. The token needs a Drive scope that allows file creation. All values below are placeholders.
const DRIVE_UPLOAD_URL =
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
function readBearerToken(req: Request): string | null {
const header = req.headers.get("authorization") ?? "";
const match = /^Bearer (.+)$/.exec(header);
return match ? match[1] : null;
}
export async function saveFileToDrive(
req: Request,
fileName: string,
fileText: string,
): Promise<Response> {
const token = readBearerToken(req);
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
const boundary = "mcp-upload-boundary";
const body =
"--" + boundary + "\r\n" +
"Content-Type: application/json; charset=UTF-8\r\n\r\n" +
JSON.stringify({ name: fileName }) + "\r\n" +
"--" + boundary + "\r\n" +
"Content-Type: text/plain\r\n\r\n" +
fileText + "\r\n" +
"--" + boundary + "--";
const driveResponse = await fetch(DRIVE_UPLOAD_URL, {
method: "POST",
headers: {
Authorization: "Bearer " + token,
"Content-Type": "multipart/related; boundary=" + boundary,
},
body,
});
// Return a 401 as is, so the caller can refresh the token and retry once.
return driveResponse;
}The handler does not store the token or write it to a log. It uses the token for the one request only. If Google answers 401, return that status. Chipp then refreshes the token and retries the call one time (see Token Refresh).
For files that your agent creates, the hosted google_drive_save_file tool is simpler. It needs no handler and no server of your own.
Security Considerations
Each user's tokens are stored separately. A user can only act through their own connection.
Per-user tokens are never returned to the model, and stored tokens are never sent to custom actions or to the external servers of hosted integrations. For a per-user MCP connector, Chipp sends the user's token only to that connector's MCP server, in the Authorization header. A delegated user connection sends only a single-use, 5-minute delegation token to one approved host, which redeems it for a new drive.file-only token.
Chipp requests only the scopes a tool needs. Users see the requested scopes during authorization.
Stored tokens are encrypted in the database.
Users can revoke access in the third-party service's settings at any time.
Troubleshooting
If you don’t see the Consumer OAuth option
The option does not exist on custom MCP servers. The Connect Custom Server dialog never had a “Consumer OAuth (per-user)” auth type.
Solution: For per-user accounts, add a supported catalog connector and turn on its Per-user accounts toggle. For your own server acting in a user’s Google Drive, choose Delegated user connection (Google Drive). For a shared credential on a custom server, choose None, Bearer Token / API Key or API Key (Custom Header).
”Invalid redirect_uri” Error
A connector’s OAuth app does not list the callback URL Chipp sends.
Solution: Register the URLs from Callback URLs (reference) exactly as written. Use the two MCP URLs for a per-user MCP connector. For a hosted integration that uses your own OAuth client, register the two hosted URLs from the second table. For iWave, register only the one hosted URL from the iWave table. Platform credentials need no registration.
”Access Denied” During Authorization
The user declined the consent prompt, or the requested scopes are not allowed.
Solution: Ask the user to try again and choose Allow. For Google Drive, a user who declined the optional permission to create files can still read files. They can allow saving files later when Chipp prompts them to reconnect.
Refresh Token Missing
The provider only issues refresh tokens on the first authorization.
Solution: The user revokes access in the provider’s settings, then reconnects.
Related Guides
- Building Custom MCP Servers - Complete guide to building MCP servers
- Google Drive Integration - Per-user Google Drive, including saving files
- Getting Started with Pre-Built MCP Integrations - Use pre-built MCP integrations
- Chipp MCP Server - Connect Claude Code to manage your Chipp apps