Skip to main content
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

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.
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. That approach needs sign-in off on the server.

What changes when you turn it on

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

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

Fill in the mode’s settings

For Hosted, enter your Login URL. See 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.
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.

Settings

login_url, issuer and audience accept ${env.NAME} references to your 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.
mcp.oauth.request returns:

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.
Three rules your page has to follow:
  • 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.
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.
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.
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.
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. 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.
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 tells you which check a real token fails.

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

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. The Metadata API exposes the same operations as the builder panel:

Limits

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

Next steps

Connecting Clients

Connect Claude, Cursor, VS Code and more to your MCP server.

Bring your own identity provider

Run your own OAuth front end with a Cloudflare Worker, for setups native sign-in doesn’t cover.