> ## 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: each person signs in once with your identity provider, and your system enforces that person's own permissions on every call

**Why:** give a shared agent access to an internal system without a shared service account. Each person signs in once; from then on the agent calls your MCP connector with that person's own token, and your system decides what they may see and do. Two people asking the same agent get different, correct answers, and every call is attributable to the person who made it.

**How it works:**

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

## Set it up

**1. Turn it on (self-hosted).** `client-auth` brokers the sign-in from its published origin. Empty, the default, turns MCP OAuth off.

```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, redirect URI `https://client-auth.<domain>/mcp_auth/*`, scopes `openid profile email offline_access`.

**3. Make the MCP connector accept the token.** Publish your identity provider's endpoints at `/.well-known/oauth-authorization-server` on the MCP connector's own origin (xpander reads it first and requests exactly its `scopes_supported`, so list only your client's scopes). Answer a missing or bad token with `401`. On every call, verify the token and act as the person in it:

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

ISSUER = "https://idp.example.internal/auth/realms/acme"   # Keycloak paths shown
keys = jwt.PyJWKClient(f"{ISSUER}/protocol/openid-connect/certs")

def person_from(authorization_header: str) -> str | None:
    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, options={"verify_aud": False})
    except jwt.PyJWTError:
        return None                                    # -> 401
    if claims.get("azp") != "acme-inventory-mcp":      # only tokens minted for your client
        return None
    return claims["email"]                             # apply this person's permissions
```

**4. Register it for the organization.** **Settings > Skills**, the list of MCP servers, **Add server**: **Shared with** **Organization**, the server URL, **Authentication** **OAuth2**, the client's **Client ID** and **Client secret**.

<Frame caption="Add server with OAuth2. Shown with sample data.">
  <img src="https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-add-server.png?fit=max&auto=format&n=7SVHNU8a6e1bTeE7&q=85&s=9522f11cd566f0f1f3acfcf73cf7bb4d" alt="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" width="1440" height="1240" data-path="images/verify/lab-mcp-oauth-add-server.png" />
</Frame>

**5. Attach it to a shared agent.** In the agent's settings, **Add skill**, pick the entry, and give the agent **Org-wide** access.

<Frame caption="Add skill lists the organization entry. Shown with sample data.">
  <img src="https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-add-skill.png?fit=max&auto=format&n=7SVHNU8a6e1bTeE7&q=85&s=7a4aa46f0e1c9e36265af7c63da63d20" alt="Add a skill panel searching inventory, listing Acme Inventory of type MCP" width="1440" height="900" data-path="images/verify/lab-mcp-oauth-add-skill.png" />
</Frame>

## What two people see

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

<Frame caption="Alice's first prompt: sign in once. Shown with sample data.">
  <img src="https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-alice-signin.png?fit=max&auto=format&n=7SVHNU8a6e1bTeE7&q=85&s=ab4ec3ff2278c7a810f0e4015a5d04fa" alt="Acme IT Assistant chat as Alice with a Sign in to Acme Inventory card" width="1440" height="900" data-path="images/verify/lab-mcp-oauth-alice-signin.png" />
</Frame>

<Frame caption="Alice gets her equipment. Shown with sample data.">
  <img src="https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-alice-answer.png?fit=max&auto=format&n=7SVHNU8a6e1bTeE7&q=85&s=68e6f21083ea6c11763108fee783b9db" alt="Acme IT Assistant answering Alice with the three items assigned to her" width="1440" height="900" data-path="images/verify/lab-mcp-oauth-alice-answer.png" />
</Frame>

<Frame caption="Bob's first prompt on the same agent: his own sign-in. Shown with sample data.">
  <img src="https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-bob-signin.png?fit=max&auto=format&n=7SVHNU8a6e1bTeE7&q=85&s=f58e19b3f3cba7ff8bca13bd1c744558" alt="Acme IT Assistant chat as Bob with a Sign in to Acme Inventory card" width="1440" height="900" data-path="images/verify/lab-mcp-oauth-bob-signin.png" />
</Frame>

<Frame caption="Bob gets his equipment, not Alice's. Shown with sample data.">
  <img src="https://mintcdn.com/xpanderai-099931d1/7SVHNU8a6e1bTeE7/images/verify/lab-mcp-oauth-bob-answer.png?fit=max&auto=format&n=7SVHNU8a6e1bTeE7&q=85&s=f7730c0bf36f0fd7f1ce180d95252c05" alt="Acme IT Assistant answering Bob with the items assigned to him" width="1440" height="900" data-path="images/verify/lab-mcp-oauth-bob-answer.png" />
</Frame>

**If it fails:** "MCP OAuth is not available on this deployment." means `CLIENT_AUTH_EXTERNAL_URL` is empty. An invalid redirect URI at the identity provider means the `/mcp_auth/*` callback is not registered. `invalid_scope` means the MCP connector's metadata lists a scope your client does not have.


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