> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xano.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Require sign-in (OAuth) on an MCP server

> Make an MCP server you build in Xano require OAuth sign-in, with Xano as the authorization server or with tokens from your own identity provider, so each user arrives in your tools as their own $auth.

An MCP server you build in Xano can require its users to sign in. With sign-in on, any MCP
client that implements MCP authorization connects by signing the user in as **one of your
own users**: a row of one of your auth tables. That includes Claude's **Add custom
connector** on web, desktop and mobile, Claude Code, Cursor, VS Code and the MCP SDKs. Your
tools keep their existing **Authentication** settings and see that user as `$auth`.

There's no proxy to run and no token in the URL. The connection URL stays the same, and
the client handles discovery, registration, sign-in and token refresh on its own.

## Choose a mode

| Mode | Who runs the authorization server | Who authenticates the user |
| - | - | - |
| **Hosted** | Xano, one per MCP server | Your own login page and backend, which also ask the user to approve the client. Xano issues the tokens. |
| **External** | Your identity provider: Auth0, WorkOS, Clerk, Microsoft Entra ID, Okta, Amazon Cognito, or another OpenID Connect provider | Your identity provider. Xano verifies its JWT access tokens and links each one to a row of your auth table. |

Pick **Hosted** if your users already sign in to your Xano backend with your own login
endpoints. Pick **External** if they sign in through an identity provider that issues JWT
access tokens.

> [!NOTE]
>
> Want to run the OAuth front end yourself instead, for example because your provider issues
> opaque tokens or you need sign-up on first login? See [Bring your own identity
> provider](/ai-tools/mcp-builder/oauth-proxy). That approach needs sign-in **off** on the
> server.

## What changes when you turn it on

> [!WARNING]
>
> **Sign-in closes the whole server.** Every request without a valid access token gets a
> `401`, including `initialize` and `tools/list`, and the challenge points the client at
> the server's sign-in. Clients that support MCP authorization follow it and start sign-in.
>
> **Session tokens stop working on this server.** A client that sends one of your
> auth-table session tokens in an `Authorization: Bearer` header gets `401` "Session tokens
> are not accepted by this MCP server." There's no setting to keep accepting them, so move
> those clients to sign-in before you turn it on.

* **Access tokens work only on this MCP server.** A token issued for it is refused by your
  regular API endpoints and by every other MCP server, including servers on the same auth
  table.
* **Servers without sign-in are unchanged.** The [Xano MCP
  Server](/ai-tools/xano-mcp-server) isn't affected either.
* **Clients need MCP authorization `2025-06-18` or later.** These follow the server's
  `resource_metadata` pointer. Clients that only look for
  `/.well-known/oauth-authorization-server` at the root of the host (the `2025-03-26`
  discovery) aren't supported.

## Turn on sign-in

**Builder**

**Step 1**

### Open the server's settings

Open your MCP server and click Edit MCP Server.
Under **MCP Server Settings**, find **Sign-in (OAuth)** and click
Configure.

**Step 2**

### Pick a mode and an auth table

Set **Sign-in** to **Hosted** or **External**, and pick the **Auth table** whose
rows are the users who sign in. Only tables with authentication enabled are listed.

**Step 3**

### Fill in the mode's settings

For **Hosted**, enter your **Login URL**. See [Hosted sign-in](#hosted-sign-in).

For **External**, pick your **Provider** and enter the **Issuer**, **Column** and,
if your provider needs it, **Audience** or **Allowed client IDs**. See [External
identity provider](#external-identity-provider).

**Step 4**

### Save and confirm

Click Save and confirm **Turn on sign-in**. The panel
then lists the **Endpoints to register** for each of the server's hosts.

**XanoScript**

Add an `oauth` block to the `mcp_server`.

```xs Hosted theme={null}
mcp_server "Support" {
  canonical = "67Dx5RNL"
  tools = [{name: "get_order_status", auth: "user"}]
  oauth = {
    mode      : "hosted"
    auth_table: "user"
    login_url : "${env.MCP_LOGIN_URL}"
  }
}
```

```xs External theme={null}
mcp_server "Support" {
  canonical = "67Dx5RNL"
  tools = [{name: "get_order_status", auth: "user"}]
  oauth = {
    mode      : "external"
    auth_table: "user"
    issuer    : "${env.MCP_ISSUER}"
    column    : "auth0_id"
    preset    : "auth0"
  }
}
```

`xano workspace push` applies it. Removing the `oauth` block from a pushed file turns
sign-in off. The block is validated on every save path: the builder, the Metadata API,
`xano workspace push`, and release and tenant deploys.

### Settings

| Field | Mode | What it does |
| - | - | - |
| `mode` | both | `hosted` or `external`. Required. |
| `auth_table` | both | The auth table whose rows sign in. It must have authentication enabled. Required. |
| `login_url` | hosted | Your login page, which is also the consent screen. Must be `https`. `http://localhost` works on development instances only. Required. |
| `issuer` | external | Your provider's issuer URL, spelled exactly as its tokens' `iss` (a trailing slash counts). Must be `https`. Required. |
| `column` | external | The auth-table column the token's claim is matched against. Required. |
| `claim` | external | The token claim to match. Default `sub`. `sub`, `oid` and `email` are accepted as they are; `email` only when the token has `email_verified: true`. Any other claim needs `claim_ack: true`, because users or admins can edit most other claims. |
| `audience` | external | Accepted `aud` values. Leave it empty to require the URL the client connected to. A value that isn't one of this server's URLs needs `audience_ack: true`. |
| `preset` | external | `auth0`, `clerk`, `workos`, `entra`, `okta`, `cognito` or `generic`. See [Provider setup](#provider-setup). |
| `allowed_client_ids` | external | If set, the token's `azp` (or `client_id`) must be one of these. Required with the `clerk` preset. |

`login_url`, `issuer` and `audience` accept `${env.NAME}` references to your [environment
variables](/building/logic/working-with-data/environment-variables), so one release works in
every environment. If a referenced variable doesn't exist, the server answers every request
with `503` "This MCP server's sign-in is not configured correctly." until you add it.

## Hosted sign-in

In hosted mode, Xano is the authorization server and **your login page** does the
signing in. Xano never renders a page your users act on, so your page also acts as the
consent screen: it shows which client is asking for access and lets the user approve or
deny it.

You need:

* An `https` page you control, set as the **Login URL**.
* Two endpoints in your workspace, authenticated on the server's auth table, that call
  `mcp.oauth.request` and `mcp.oauth.complete`.

### How the flow works

1. The client registers itself and sends the user's browser to the server's authorize URL.
2. Xano checks the client, its callback URL and PKCE, then redirects the browser to your
   **Login URL** with `?mcp_request=<id>` added.
3. Your page signs the user in, shows what's asking for access, and asks them to approve or
   deny it.
4. Your page calls your own endpoint, which runs `mcp.oauth.complete` with the user's
   decision and returns a **continue URL**.
5. Your page navigates the browser to the continue URL.
6. Xano sends the browser back to the client, with an authorization code if the user
   approved, or `access_denied` if they denied.
7. The client exchanges the code for an access token and a refresh token, and connects.

### Build the login handoff endpoints

`mcp.oauth.request` reads what's asking for access. `mcp.oauth.complete` records the user's
answer and returns the continue URL. Both run in an endpoint that requires authentication on
the server's auth table, in the same workspace as the MCP server.

```xs theme={null}
query "mcp/request" verb=GET {
  api_group = "auth"
  auth = "user"

  input {
    text mcp_request
  }

  stack {
    mcp.oauth.request {
      request = $input.mcp_request
    } as $sign_in
  }

  response = $sign_in
}

query "mcp/complete" verb=POST {
  api_group = "auth"
  auth = "user"

  input {
    text mcp_request
    // "approve" or "deny"; anything else is refused
    text decision
  }

  stack {
    mcp.oauth.complete {
      request = $input.mcp_request
      decision = $input.decision
    } as $continue_url
  }

  response = {continue_url: $continue_url}
}
```

`mcp.oauth.request` returns:

| Field | What it is |
| - | - |
| `client_name` | The name of the client asking for access. |
| `callback_host` | The host the user's access is sent back to. Empty for a native-app callback with no host. |
| `client_verified` | `true` when the client is identified by a Client ID Metadata Document, so its name comes from its own URL. `false` for a dynamically registered client, which named itself. |
| `client_logo` | An `https` URL, or empty. |
| `server_name` | Your MCP server's name. |

### Build the login page

Your page reads `mcp_request` from its query string, signs the user in, asks them, and hands
the answer back. Keep `mcp_request` through your own redirects, such as social login or MFA.
It expires 10 minutes after the client started the flow.

```js theme={null}
const mcpRequest = new URLSearchParams(location.search).get("mcp_request");

// 1. Sign in with your existing login endpoint. This mints a fresh session token.
const { authToken } = await post("/api:auth/auth/login", { email, password });
const auth = { Authorization: `Bearer ${authToken}` };

// 2. Show what is asking for access, and ask the user.
const signIn = await get(`/api:auth/mcp/request?mcp_request=${encodeURIComponent(mcpRequest)}`, auth);
const decision = await askUser(
  `${signIn.client_name} (returns to ${signIn.callback_host}) wants to access ${signIn.server_name}`
);

// 3. Hand the answer to Xano, authenticated with that fresh token.
const { continue_url } = await post("/api:auth/mcp/complete", { mcp_request: mcpRequest, decision }, auth);

// 4. Top-level navigation, in the same browser that arrived with mcp_request.
window.location.assign(continue_url);
```

Three rules your page has to follow:

> [!WARNING]
>
> * **Show the client name and the callback host before the user approves.** The callback
>   host is where the user's access goes. Showing it is what protects your users from a
>   malicious client that registered itself under a familiar name.
> * **Use a fresh session token.** `mcp.oauth.complete` signs in the user authenticated on
>   the request that calls it, and that token must have been issued in the last 10 minutes.
>   A token you stored earlier is refused with `stale_session`. Sign the user in, then use
>   the token you just got.
> * **Navigate to the continue URL, don't `fetch` it.** It's single-use, expires after 5
>   minutes, and only works in the browser that started the flow, on the host it started on.

Xano doesn't remember approvals: your page decides on every sign-in. It can skip the
question for clients you trust, or remember a user's earlier answer itself.

To let the user back out before signing in, send the browser to the cancel URL. It's the
server's authorize URL with `/authorize` replaced by `/cancel`, plus the same
`mcp_request`. The client gets `access_denied`. A user who's already signed in and declines
should go through `mcp.oauth.complete` with `decision = "deny"` instead.

```text Shape theme={null}
https://{instance-host}/x2/mcp/{canonical}/oauth/cancel?mcp_request={id}
```

```text Example theme={null}
https://x8ki-letl-twmt.n7.xano.io/x2/mcp/67Dx5RNL/oauth/cancel?mcp_request=mr_8f2c1d
```

> **Refusals from mcp.oauth.request and mcp.oauth.complete**
>
> Both functions refuse with HTTP `400` and one of these messages, also returned as
> `reason` in the error payload. Catch them with `try_catch` and branch on
> `$error.message`.
>
> | Message | Meaning | What to do |
> | - | - | - |
> | `request_expired` | The request is more than 10 minutes old, or the `mcp_request` isn't one Xano issued. | Send the browser to the cancel URL, or ask the user to start again from their app. |
> | `request_used` | The request was already completed or cancelled. | Ask the user to start again from their app. |
> | `wrong_tenant` | Your endpoint runs in a different tenant than the MCP server. | Call an endpoint in the server's tenant. |
> | `wrong_server` | Your endpoint runs in a different workspace, or the server no longer uses hosted sign-in. | Call an endpoint in the server's workspace. |
> | `wrong_table` | The authenticated user isn't a row of the server's auth table. | Authenticate the endpoint on the right auth table. |
> | `stale_session` | The session token was issued more than 10 minutes ago. | Mint a fresh token at login and use that one. |
> | `datasource_mismatch` | Your endpoint runs on a different datasource than the sign-in started on (normally `live`). | Call the endpoint without a different `X-Data-Source`. |

> **unsafe\_user\_id: signing in a user without \$auth**
>
> `mcp.oauth.complete` accepts `unsafe_user_id` to name the row directly. It **skips both
> the `$auth` check and the 10-minute session check**, so anyone who can reach an endpoint
> that passes it can sign in as that user.
>
> ```xs theme={null}
> mcp.oauth.complete {
>   request = $input.mcp_request
>   decision = $input.decision
>   unsafe_user_id = $user.id
> } as $continue_url
> ```
>
> Use it only in a custom login flow whose own stack has already proven who the user is,
> and never take the value from request input.

> **Tenants and datasources**
>
> A server inside a tenant is signed in to through that tenant, so the endpoint that calls
> `mcp.oauth.complete` must run in the same tenant. Otherwise it gets `wrong_tenant`.
>
> The datasource is recorded when the client starts the flow: from an `x-data-source`
> query parameter on the authorize URL, or `live` otherwise. The endpoint that calls
> `mcp.oauth.complete`, and every MCP request made with the resulting token, must use the
> same datasource.

### Clients, tokens and sessions

* **Client registration:** clients can use Dynamic Client Registration (public clients
  only, 1 to 10 redirect URIs) or a Client ID Metadata Document (an `https` `client_id`).
  A dynamic registration belongs to one server and one branch, and expires after 30 days
  unused.
* **Callbacks:** `https` URLs, loopback addresses on any port, and native-app schemes.
* **Grant type:** authorization code with PKCE `S256` only. No scopes are advertised.
* **Access tokens** last 1 hour. **Refresh tokens** rotate on every use. Reusing one that
  was already rotated away revokes the whole sign-in, except for a single replay within 60
  seconds, for a client that lost a response.
* **A sign-in ends** after 30 days without a refresh, or 90 days after it started. Each
  completed sign-in is its own grant, so a user signed in from two installations of the same
  client has two grants.

On every request, Xano re-checks that the grant isn't revoked, the user's row still exists,
the datasource matches, and the server's sign-in settings haven't changed.

Signed-in sessions are listed under **Signed-in clients** in the **Sign-in (OAuth)** panel,
with the client, the user, and when they signed in and last refreshed. Click
Revoke to end one.

## External identity provider

In external mode, your identity provider signs users in and issues the tokens. Xano checks
each access token on every request and matches it to exactly one row of your auth table.

| Builder field | XanoScript | What to enter |
| - | - | - |
| **Provider** | `preset` | Your provider. Picking one sets **Claim** to its stable user ID (`sub`, or `oid` for Entra) and shows any setup notes for it. |
| **Issuer** | `issuer` | Your provider's issuer, matched exactly. |
| **Audience** | `audience` | One per line. Leave empty to accept this server's own URLs. |
| **Allowed client IDs** | `allowed_client_ids` | One per line, matched against `azp` or `client_id`. Shown for Clerk. |
| **Claim** | `claim` | The token claim that identifies the user. |
| **Column** | `column` | The auth-table column the claim is matched against, such as `auth0_id`. |

There's no JWKS field: Xano reads `<issuer>/.well-known/openid-configuration`, requires its
`issuer` to match yours exactly, and fetches the keys from its `jwks_uri`.

> [!WARNING]
>
> **There's no automatic sign-up.** Exactly one row must have the claim's value in **Column**.
> If no row matches, or two do, the request gets `401`. Create or link each user's row before
> they connect.

### Provider setup

After you save, **Endpoints to register** lists, for each of the server's hosts, the
**Resource** (the connection URL, which is also the audience to register) and the
**Protected resource metadata** URL. With **Audience** left empty, your provider must put the
URL the client connected to into `aud`.

These notes are based on each provider's documented behavior. Check them against your
provider's current docs; [Test a token](#test-a-token) tells you which check a real token
fails.

| Provider | Issuer | Audience | Claim / column | Notes |
| - | - | - | - | - |
| **Auth0** | `https://<tenant>.<region>.auth0.com/`, with the trailing slash | Create an API whose identifier is the MCP URL, and turn on the **Resource Parameter Compatibility Profile**. Without it, Auth0 doesn't issue a JWT for your API. | `sub`, into a column holding the Auth0 user ID (`auth0\|...`) | MCP clients must register with Auth0: enable Dynamic Client Registration, or pre-register them. |
| **WorkOS AuthKit** | Your AuthKit domain, such as `https://<name>.authkit.app` | Leave empty. AuthKit puts the requested resource in `aud`. | `sub` | Enable MCP / Dynamic Client Registration in AuthKit. |
| **Clerk** | Your Clerk Frontend API URL | Clerk's access tokens carry no `aud`, so set **Allowed client IDs**. | `sub` | Only clients you list can sign in, so dynamically registered clients don't work unless you add their IDs. Tokens must be JWTs. |
| **Microsoft Entra ID** | `https://login.microsoftonline.com/<tenant-id>/v2.0` (v2 access tokens) | Your app's client ID or Application ID URI, with `audience_ack: true`. | `oid` | Entra doesn't support Dynamic Client Registration, so MCP clients need a pre-registered app. |
| **Okta** | A custom authorization server, such as `https://<org>.okta.com/oauth2/default`. Tokens from the org authorization server can't be verified. | The custom server's audience (such as `api://default`), with `audience_ack: true` unless it's the MCP URL. | `sub` is the user's login. The stable ID is `uid`, which needs `claim_ack: true`. | |
| **Amazon Cognito** | `https://cognito-idp.<region>.amazonaws.com/<user-pool-id>` | Cognito access tokens carry no `aud` unless you use resource binding, and Xano refuses a token with no `aud`. | `sub` | The preset also requires `token_use: "access"`. |
| **Other OIDC** (`generic`) | The provider's issuer | Whatever it puts in `aud` | `sub` | The provider must serve `<issuer>/.well-known/openid-configuration` and issue JWT access tokens. |

### Test a token

In the **Sign-in (OAuth)** panel, paste an access token from your provider under **Test a
token** and click Test token. Xano checks it against the
**saved** settings and shows whether it's accepted. If it isn't, you see the check that failed
and why, and how many rows matched. The token isn't stored.

> **Every check a token goes through**
>
> 1. It's a signed JWT: `RS256`–`512`, `PS256`–`512`, `ES256`/`384`/`512` or `EdDSA`.
>    `none` and HMAC are refused.
> 2. It's signed by a key from your issuer's key set, found by `kid`. Keys are cached for 10
>    minutes. A new `kid` triggers a refetch, at most once every 30 seconds. If your provider
>    is unreachable, cached keys keep working.
> 3. `iss` equals your issuer exactly.
> 4. `exp` is in the future, and `nbf`/`iat` aren't, with 60 seconds of leeway.
> 5. `aud` contains the URL the client connected to, or one of your **Audience** values.
> 6. `azp` (or `client_id`) is in **Allowed client IDs**, when set.
> 7. It's an access token. Tokens carrying `nonce` or `at_hash` (ID tokens) are refused.
> 8. Exactly one row has the claim's value in **Column**, on the request's datasource.

A session lasts until the token's `exp`. Xano keeps no grants in external mode, so to cut a
user off immediately, change or remove their row so the claim no longer matches. Revoking at
your provider stops new tokens, not ones already issued.

## Inside your tools

* **`$auth` is the signed-in row**, just as with a session token: `$auth.id` is the row ID,
  and a tool whose **Authentication** uses the same table passes. `$auth.extras` is empty.
* **Every tool, prompt and resource on the server must use the sign-in auth table, or be
  public.** The signed-in user is always a row of that table, so an item that requires a
  different table always fails. The builder doesn't warn you about this.
* **Don't forward the access token.** `$env.$request_auth_token` and `$http_headers` hold the
  MCP access token the client presented. Your regular endpoints refuse it.
* **The [connection trigger](/building/logic/triggers/mcp-servers)** receives
  `$input.auth`, with the signed-in row's `{dbo, id, extras}`. Use it to narrow the tools,
  prompts and resources each user sees.
* **AI agents don't run as the user.** Tools called by an agent from `ai.agent.run` inside a
  tool don't carry the signed-in user, so agent tools that require authentication fail with
  "Invalid token."
* `security.create_auth_token` inside a tool still mints a normal session token.

## Revoking access

| How | Effect |
| - | - |
| **Revoke** under **Signed-in clients** | Ends one grant (hosted). |
| `mcp.oauth.revoke` in a function stack | Ends sign-ins on a server for one user (`user_id`), one grant (`grant_id`), or everyone (`all: true`). Pass exactly one. Returns `{revoked: <count>}`. Hosted only. |
| Deleting the user's row | Ends every sign-in for that user. |
| Changing a setting that [signs everyone out](#what-signs-everyone-out) | Ends every sign-in on the server. |
| Deleting the MCP server | Removes its grants and registered clients. |

```xs theme={null}
mcp.oauth.revoke {
  mcp_server = "support"
  user_id = $input.user_id
} as $revoked
```

Revocation takes effect on the client's next request.

### What signs everyone out

Every session is bound to the server's mode, auth table, claim, column, issuer, audience,
provider and allowed client IDs. Changing any of them ends every session on the next request,
and clients sign in again. That includes a change in the value of an environment variable
that **Issuer** or **Audience** refers to. Changing the **Login URL** doesn't sign anyone out.

## Branches, hosts and deploys

* **Each branch signs in separately.** A non-live branch (`67Dx5RNL:dev`) has its own
  issuer, its own registered clients and its own tokens. Its tokens don't work on the live
  branch.
* **Sign-in works only on the server's own hosts:** your instance host, its custom domains,
  the workspace's generated host and, inside a tenant, the tenant's domain. Anywhere else,
  the server answers `401` "This MCP server cannot be signed in to at this address."
* **The `oauth` settings travel** with `xano workspace push`, releases, imports and branch
  clones. Grants and registered clients don't, so users sign in again after a clone or
  import.

## Discovery endpoints

You don't need these to connect a client, which finds them on its own. They're useful when
you register the server with your identity provider or debug a client. The **Endpoints to
register** list in the builder shows the exact URLs for each host.

| What | URL |
| - | - |
| Challenge | `401` with `WWW-Authenticate: Bearer resource_metadata="…"` |
| Protected resource metadata (RFC 9728) | `https://{host}/.well-known/oauth-protected-resource/x2/mcp/{canonical}/mcp/stream` |
| Issuer (hosted) | `https://{host}/x2/mcp/{canonical}/oauth` |
| Authorization server metadata (hosted, RFC 8414) | `https://{host}/.well-known/oauth-authorization-server/x2/mcp/{canonical}/oauth` |
| Authorize, token, register, revoke (hosted) | `{issuer}/authorize`, `{issuer}/token`, `{issuer}/register`, `{issuer}/revoke` |

The Metadata API exposes the same operations as the builder panel:

| Operation | Metadata API route |
| - | - |
| List grants | `GET workspace/{workspace_id}/mcp_server/{mcp_server_id}/oauth/grant` |
| Revoke a grant | `POST workspace/{workspace_id}/mcp_server/{mcp_server_id}/oauth/grant/{grant_id}/revoke` |
| Test an external token | `POST workspace/{workspace_id}/mcp_server/{mcp_server_id}/oauth/test_token` |
| Endpoints to register | `GET workspace/{workspace_id}/mcp_server/{mcp_server_id}/oauth/endpoints` |

## Limits

> **Rate limits and known limitations**
>
> **Rate limits**
>
> * Authorize: 60 requests per minute per IP, 600 per minute per server, and at most 500
>   sign-ins in progress per server.
> * Token: 120 per minute per IP, 1,200 per server. Revoke: 60 per IP, 600 per server.
> * Registration: 100 per hour per IP, 1,000 per server. Client ID Metadata Document
>   fetches: 30 per minute per IP, 300 per server.
> * Token tests: 20 per minute per server.
>
> **Known limitations**
>
> * Scopes aren't mapped to tools. A signed-in user reaches every tool their
>   **Authentication** settings allow.
> * External mode has no automatic sign-up.
> * Agent tools called from `ai.agent.run` inside a tool don't run as the signed-in user.
> * A self-hosted or local instance with no ingress host configured can't start sign-in.

## Troubleshooting

| Symptom | Likely cause |
| - | - |
| `401` "Session tokens are not accepted by this MCP server" | The client sends a session token in an `Authorization` header. Remove the header and let the client sign in. |
| `503` "This MCP server's sign-in is not configured correctly." | A `${env.NAME}` in **Login URL**, **Issuer** or **Audience** refers to a variable that doesn't exist. |
| `401` "This MCP server cannot be signed in to at this address." | The client connected through a host that isn't one of the server's own. Use the connection URL from the builder. |
| The client never prompts the user to sign in | It doesn't support MCP authorization `2025-06-18` or later. |
| A tool call fails right after sign-in | The tool requires a different auth table from the sign-in **Auth table**. |
| `stale_session` from `mcp.oauth.complete` | Your page reused an older session token. Mint a fresh one at login. |
| "This sign-in was started in a different browser." | The continue URL was opened outside the browser that started the flow, or with `fetch`. |
| An external token is refused | Use [Test a token](#test-a-token) to see which check fails. |

## Next steps

> **[Connecting Clients](/ai-tools/mcp-builder/connecting-clients)**
>
> Connect Claude, Cursor, VS Code and more to your MCP server.

> **[Bring your own identity provider](/ai-tools/mcp-builder/oauth-proxy)**
>
> Run your own OAuth front end with a Cloudflare Worker, for setups native sign-in doesn't cover.


## Related topics

- [MCP Builder](/ai-tools/mcp-builder.md)
- [MCP Functions](/ai-tools/mcp-builder/mcp-functions.md)
- [Connecting Clients](/ai-tools/mcp-builder/connecting-clients.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.