Developer Platform

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

|View as Markdown
Hunter Hodnett
Hunter HodnettCPTO at Chipp
||1 min read
#mcp#oauth#integrations#authentication#google-drive#advanced

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:

OptionUse it when
NoneThe MCP server is public and needs no credentials
Bearer Token / API KeyThe 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:

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

  1. The end-user connects Google Drive once, inside Chipp.
  2. When the AI calls your action or MCP tool, Chipp finds that user’s Drive connection and checks it allows saving files.
  3. Chipp signs a short-lived delegation token and sends it to your server in a request header.
  4. Your server exchanges the token with Chipp. At that moment Chipp asks Google for a new access token limited to drive.file only, and returns it.
  5. 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

  1. Choose Delegated user connection (Google Drive) as the authentication option.
  2. Set the fields:
FieldValue
Provider connectionGoogle Drive (fixed)
Required permissiondrive.file (fixed)
Approved destination hostThe one host allowed to receive the token. It is prefilled from your URL and must equal it.
Header nameDefaults 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.com does not allow evil.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 3xx response 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

PropertyValue
FormatSigned JWT (HS256)
Lifetime5 minutes at most
UseSingle use. A second redemption fails.
AudienceThe destination origin, https://<destination host>
Bound toThe 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:

http
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:

json
{
  "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

CodeMeaningWhat to doExchange HTTP status
DELEGATION_CONSUMER_IDENTITY_REQUIREDNo signed-in user for this session (anonymous, voice, or a builder test)Ask the user to sign in403
DELEGATION_CONNECTION_REQUIREDThe user has not connected Google DriveUser connects Google Drive in chat409
DELEGATION_INSUFFICIENT_SCOPEThe connection lacks drive.fileUser reconnects and approves file access403
DELEGATION_GRANT_REVOKEDThe Google grant expired or was revoked (Google answered invalid_grant)User reconnects Google Drive409
DELEGATION_DESTINATION_MISMATCHThe 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 changedFix the action URL or the destination host. Do not retry.403
DELEGATION_FAILEDBad signature, expired token, a token used twice, Google returned a token with scopes other than drive.file, or an internal failureTry 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:

  1. Open your app, click Brain, and open the Integrations tab. Click Browse all integrations and select the catalog connector.
  2. Turn on the Per-user accounts toggle. Each end-user then authorizes with their own account.
  3. Click Save & Enable. This creates the integration.
  4. 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

Architecture

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.

  1. User chats with your Chipp app
  2. The AI agent calls a tool that needs the user’s account
  3. Chipp detects the user has not connected that account
  4. The user sees a connect prompt and clicks it
  5. The provider’s consent page opens and the user authorizes access
  6. The callback returns an authorization code to Chipp
  7. Chipp exchanges the code for access and refresh tokens
  8. Tokens are stored securely per user
  9. 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:

SettingValueOrigin
Per-user callback (runtime)https://dino-mullet.chipp.ai/consumer/mcp/oauth/callbackAPP_URL (the Chipp API origin)
Builder MCP connection callbackhttps://build.chipp.ai/api/integrations/mcp/oauth/callbackWEB_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:

CredentialsPer-user sign-in callbackBuilder sign-in callback
Chipp platform credentials (Chipp’s own OAuth client)https://dino-mullet.chipp.ai/api/integrations/hosted/oauth/callbackhttps://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/callbackhttps://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:

CredentialsPer-user sign-in callbackBuilder sign-in callback
Chipp platform credentials or your own OAuth clienthttps://dino-mullet.chipp.ai/api/integrations/hosted/oauth/callbackhttps://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=S256 in the authorization request. It sends the matching code_verifier when it exchanges the code.
  • Grants: The request uses grant_type=authorization_code for the first exchange and grant_type=refresh_token for a refresh.
  • Body format: The body is application/x-www-form-urlencoded.
  • Client authentication: The connector uses one of three styles:
    • form_post - Chipp sends client_id and client_secret in the body.
    • form_basic - Chipp sends the client ID and secret in an HTTP Basic Authorization header, and also sends client_id in the body.
    • json_basic - Chipp sends the client ID and secret in an HTTP Basic Authorization header only. The body has no client_id.
  • Public clients: A public client has no client secret. Chipp sends client_id only, with no Authorization header.
  • Optional parameters: If the connector sets a resource indicator (RFC 8707), Chipp adds resource to the authorization, exchange and refresh requests. Some connectors (for example Meta) use a config_id in place of scope in the authorization request.

Token Refresh

Access tokens expire. Chipp refreshes them automatically:

  1. 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.
  2. Reactive refresh: If the connector’s server answers a call with 401, Chipp refreshes the token and retries the call one time.
  3. 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.
  4. Transient refresh error: If the refresh fails for a temporary reason (a network error or a 5xx response), Chipp falls back to the stored token. That token is stale but not yet expired. A later 401 triggers 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

  1. First time: the user sees a prompt to connect their account
  2. Click to connect: the provider’s login and consent page opens
  3. Authorize: the user grants access
  4. Automatic return: the window closes and the chat continues
  5. 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:

http
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:

ts
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.

ts
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

&#x1F534;
Token IsolationCritical

Each user's tokens are stored separately. A user can only act through their own connection.

&#x1F534;
Tokens Reach Only the Connector's ServerCritical

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.

&#x1F534;
Minimal ScopesCritical

Chipp requests only the scopes a tool needs. Users see the requested scopes during authorization.

&#x2B55;
Encryption at Rest

Stored tokens are encrypted in the database.

&#x2B55;
Revocation Support

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.