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

# Per-person sign-in for MCP connectors

> Let one shared agent reach an internal system as whoever is asking, with that person's own permissions.

Per-person sign-in replaces a shared service account. Each person signs in once with your identity provider, and the agent calls your MCP connector with their own token. Your system decides what each person may see and do, and every call is attributable to them.

![Diagram](https://docs.xpander.ai/images/diagrams/run-integrations-mcp-per-person-sign-in-06ffbbda.svg)

## How it works

1. The first time a request needs the MCP connector, the chat shows a **Sign in to** card. The person signs in with your identity provider (no password if they already use it for xpander).
2. xpander keeps their token in the vault and refreshes it.
3. Every call carries `Authorization: Bearer <their token>`, and the MCP connector answers as that person.

## Set it up

### 1. Turn on the sign-in broker

The `client-auth` service runs each sign-in and keeps the token.

| Install | What to do | Redirect URI to register at your identity provider |
| - | - | - |
| xpander cloud | Nothing. On by default. | `https://client-auth.xpander.ai/mcp_auth/callback` |
| Hybrid | Nothing in the chart. Sign-in runs in xpander cloud, which keeps the token. Your cluster makes the MCP calls. xpander cloud must reach your identity provider's token endpoint and the MCP connector's metadata over the internet. | `https://client-auth.xpander.ai/mcp_auth/callback` |
| Air-Gapped | Set `CLIENT_AUTH_EXTERNAL_URL` to the published `client-auth` origin. Empty (the default) means MCP OAuth is off. The token stays in the install's vault. | `https://client-auth.<domain>/mcp_auth/callback` |

Air-Gapped values:

```yaml theme={"dark"}
client-auth:
  env:
    CLIENT_AUTH_EXTERNAL_URL: https://client-auth.<domain>
```

### 2. Create a client at your identity provider

Confidential, authorization code flow, the redirect URI above (also shown as **Redirect URL** in step 4), scopes `openid profile email offline_access`. The access token must name your MCP connector in its audience (`aud`). xpander does not send the RFC 8707 `resource` parameter, so the identity provider must add it. In Keycloak, add an **Audience** mapper to the client's dedicated scope.

### 3. Make the MCP connector accept the token

xpander finds your identity provider from the MCP connector's origin, using the first address that answers:

1. `/.well-known/oauth-authorization-server` on the MCP connector's origin (RFC 8414).
2. `/.well-known/oauth-protected-resource` plus the server URL's path (RFC 9728). xpander reads the first `authorization_servers` entry, then that server's RFC 8414 metadata.

xpander requests exactly the metadata's `scopes_supported`, so list only your client's scopes.

Answer a missing or bad token with `401` and a `WWW-Authenticate` header:

```http theme={"dark"}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://inventory.example.internal/.well-known/oauth-protected-resource/mcp"
```

Any `401` or `403`, or any `WWW-Authenticate` header, means "sign-in needed" to xpander. It uses the addresses above, not the header's `resource_metadata` link. Send the header anyway for other MCP clients.

On every call, verify the token, check it was issued for your MCP connector, and act as the person in it:

```python theme={"dark"}
import jwt

ISSUER = "https://idp.example.internal/auth/realms/acme"   # Keycloak paths shown
AUDIENCE = "acme-inventory-mcp"                             # your connector, as the identity provider puts it in aud
keys = jwt.PyJWKClient(f"{ISSUER}/protocol/openid-connect/certs")

def person_from(authorization_header: str | None) -> str | None:
    """The caller's verified email, or None. Answer None with 401 and WWW-Authenticate."""
    if not authorization_header or not authorization_header.startswith("Bearer "):
        return None                                    # no token: 401, not a crash
    token = authorization_header.removeprefix("Bearer ").strip()
    try:
        key = keys.get_signing_key_from_jwt(token).key
        claims = jwt.decode(token, key, algorithms=["RS256"], issuer=ISSUER, audience=AUDIENCE)
    except jwt.PyJWTError:
        return None                                    # bad, expired or foreign token
    if claims.get("email_verified") is not True or not claims.get("email"):
        return None                                    # an unverified email is not an identity
    return claims["email"]                             # apply this person's permissions
```

* **Verified email only.** Anyone can type an email into a profile. `email_verified` says the identity provider checked it. If you can map people by `sub` (the stable user id), use that instead.
* **Keycloak:** **Verify email** is off by default (**Realm settings > Login**), so users carry `email_verified: false` and the sample refuses them. Turn it on, or set **Email verified** per user. A refused person just sees the **Sign in to** card again, so fix this first.
* **Audience check.** The MCP spec forbids accepting a token not issued for the server. If your identity provider cannot add an audience, check `azp` equals the step 2 client instead. That is weaker: it trusts every token issued to that client.

### 4. Register it for the organization

**Settings > Skills** > **MCP servers** tab > **Add server**: **Shared with** **Organization**, the server URL, **Authentication** **OAuth2**, the **Client ID** and **Client secret**. Admins can add it from **Platform > Agents > Skills registry** > **Add item** > **MCP server** > **Add a server** (lands as **Optional**). Owners can add it from **Agent settings > Skills > Add skill > Custom skill > MCP Connector** with **Add to organization registry** on ([where to register](https://docs.xpander.ai/run/integrations/mcp-gateway.md#outbound-skills-served-over-mcp)).

![Add server form with Name Acme Inventory, Shared with Organization, Remote, Server URL, HTTP, Authentication OAuth2, Client ID, a masked Client secret and the Redirect URL](https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-add-server.png?fit=max\&auto=format\&n=7SVHNU8a6e1bTeE7\&q=85\&s=9522f11cd566f0f1f3acfcf73cf7bb4d)

Add server with OAuth2. Shown with sample data.

### 5. Attach it to a shared agent

In the agent's settings, **Add skill**, pick the entry (shown as **Organization**), and give the agent **Org-wide** access.

![Add a skill panel searching inventory, listing Acme Inventory of type MCP](https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-add-skill.png?fit=max\&auto=format\&n=7SVHNU8a6e1bTeE7\&q=85\&s=7a4aa46f0e1c9e36265af7c63da63d20)

Add skill lists the organization entry. Shown with sample data.

## What two people see

Alice and Bob ask the same agent "What equipment do I have?". Each signs in once, then gets only their own equipment.

![Acme IT Assistant chat as Alice with a Sign in to Acme Inventory card](https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-alice-signin.png?fit=max\&auto=format\&n=7SVHNU8a6e1bTeE7\&q=85\&s=ab4ec3ff2278c7a810f0e4015a5d04fa)

Alice's first prompt: sign in once. Shown with sample data.

![Acme IT Assistant answering Alice with the three items assigned to her](https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-alice-answer.png?fit=max\&auto=format\&n=7SVHNU8a6e1bTeE7\&q=85\&s=68e6f21083ea6c11763108fee783b9db)

Alice gets her equipment. Shown with sample data.

![Acme IT Assistant chat as Bob with a Sign in to Acme Inventory card](https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-bob-signin.png?fit=max\&auto=format\&n=7SVHNU8a6e1bTeE7\&q=85\&s=f58e19b3f3cba7ff8bca13bd1c744558)

Bob's first prompt on the same agent: his own sign-in. Shown with sample data.

![Acme IT Assistant answering Bob with the items assigned to him](https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-bob-answer.png?fit=max\&auto=format\&n=7SVHNU8a6e1bTeE7\&q=85\&s=f7730c0bf36f0fd7f1ce180d95252c05)

Bob gets his equipment, not Alice's. Shown with sample data.

## Limits

* **Runs nobody started.** A schedule, a webhook, or a Slack, Teams or email message from a non-member has no person to sign in. The call uses the agent's **Default background user** (agent settings, **Skills > Advanced**) or the server's **User for background runs**, who must have signed in once. With none set, the call is refused. See [What an unattended run may use](https://docs.xpander.ai/use/agents/schedules.md#what-an-unattended-run-may-use). A message from a member runs as that member.
* **Who can read the answers.** An answer fetched with a person's token stays in their conversation. On an **Org-wide** agent, every member can open that conversation, and the creator can on any agent. For owner-only data, share the agent with named people instead. See [Permissions](https://docs.xpander.ai/use/agents/permissions.md#on-a-conversation).

## Disconnect a sign-in

**Settings > Connections** > **Sign-ins** lists every app and MCP server you signed in to. Use the row's **Disconnect**, or **Disconnect all** in the table's header menu. Pasted credentials stay.

xpander deletes its copy of the token but does not revoke it at your identity provider. End your session there to revoke it. The next request shows the **Sign in to** card again.

## If it fails

| Message | Cause |
| - | - |
| "MCP OAuth is not available on this deployment." on Air-Gapped | `CLIENT_AUTH_EXTERNAL_URL` is empty. |
| An invalid redirect URI at the identity provider | The step 1 redirect URI is not registered. |
| `invalid_scope` | The MCP connector's metadata lists a scope your client does not have. |

## Next steps

> **[Register MCP endpoints](https://docs.xpander.ai/run/integrations/mcp-gateway.md#outbound-skills-served-over-mcp)**
>
> Where skills are added.

> **[Choose whose credential an agent uses](https://docs.xpander.ai/use/agents/permissions.md#on-a-conversation)**
>
> Agent and conversation permissions.

> **[Set a user for background runs](https://docs.xpander.ai/use/agents/schedules.md#what-an-unattended-run-may-use)**
>
> Unattended runs.


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