# Descope (/docs/integrations/descope)

## Installation

Install the Descope plugin:

<TerminalTabs
  tabs={[
  {
    label: "pnpm",
    value: "pnpm",
    content: "pnpm i @xmcp-dev/descope",
  },
  {
    label: "npm",
    value: "npm",
    content: "npm i @xmcp-dev/descope",
  },
  {
    label: "yarn",
    value: "yarn",
    content: "yarn add @xmcp-dev/descope",
  },
  {
    label: "bun",
    value: "bun",
    content: "bun add @xmcp-dev/descope",
  },
]}
  defaultTab="pnpm"
/>

## Descope Setup

MCP clients use Dynamic Client Registration (DCR) and OAuth 2.1 to authenticate. Configure your Descope project so your xmcp server can participate in the agentic OAuth flow.

### Create an MCP Server Resource

1. Go to **Descope Console** → **Agentic Identity Hub** → **Resources**
2. Click **Create Resource** → **MCP Server**
3. Set a name and your server's base URL (e.g., `http://127.0.0.1:3001` for local development)
4. Copy the **Issuer URL**

The issuer URL identifies your resource and contains your project ID, which the plugin parses out automatically. If you'd rather not rely on parsing, pass `projectId` explicitly (also visible in **Descope Console** → **Project Settings**).

## Environment Variables

Configure the following environment variables in your `.env` file:

```bash
# Descope credentials
DESCOPE_ISSUER_URL=https://api.descope.com/v1/apps/agentic/your-project-id/your-mcp-server-id
DESCOPE_PROJECT_ID=your-project-id

# App configuration
BASE_URL=http://127.0.0.1:3001

# Scopes that are supported
SCOPES_SUPPORTED="openid,profile,email"
```

<Callout variant="info">
  For production, set `BASE_URL` to your deployed server URL and update the base URL on your MCP Server record in the Descope Console to match.
</Callout>

### Create a Management Key (optional)

<Callout variant="info">
  Required only when using getUser() or getManagementClient()
</Callout>

A management key is required to call `getUser()` or `getManagementClient()`. If you only need session data from the token, you can skip this step.

1. Go to **Descope Console** → **Company** → **Management Keys**
2. Click **+ Management Key**
3. Copy and save the key to your `.env` file since it is only shown once

```bash
# Descope Project Management Key
DESCOPE_MANAGEMENT_KEY=your-management-key
```

## Set up the Provider

Create a `middleware.ts` file in your xmcp app's `src` directory:

```typescript title="src/middleware.ts"
import { descopeProvider } from "@xmcp-dev/descope";

export default descopeProvider({
  issuerURL: process.env.DESCOPE_ISSUER_URL!,
  baseURL: process.env.BASE_URL!,
  projectId: process.env.DESCOPE_PROJECT_ID,
});
```

To enable user profile lookups, add the management key:

```typescript title="src/middleware.ts"
import { descopeProvider } from "@xmcp-dev/descope";

export default descopeProvider({
  issuerURL: process.env.DESCOPE_ISSUER_URL!,
  baseURL: process.env.BASE_URL!,
  projectId: process.env.DESCOPE_PROJECT_ID,
  managementKey: process.env.DESCOPE_MANAGEMENT_KEY,
  scopesSupported: process.env.SCOPES_SUPPORTED?.split(","),
});
```

### Configuration Options

* **`issuerURL`**: Issuer URL from your MCP Server resource in the Descope Console (required). Contains your project ID, which is parsed out automatically.
* **`baseURL`**: Base URL of your MCP server (required). Must match the URL configured on the resource in Descope.
* **`projectId`**: (Optional) Descope project ID. Pass this to skip parsing it out of `issuerURL`.
* **`managementKey`**: (Optional) Descope management key. Required to use `getUser()` or `getManagementClient()`.
* **`scopesSupported`**: (Optional) Array of OAuth scopes advertised in the resource metadata. Defaults to `["openid", "profile", "email"]`.

## Get a user session

Access the authenticated user's session in your tools using `getSession()`.

### Example: Return the current user's identity

```typescript title="src/tools/whoami.ts"
import type { ToolMetadata } from "xmcp";
import { getSession } from "@xmcp-dev/descope";

export const metadata: ToolMetadata = {
  name: "whoami",
  description: "Returns the authenticated Descope session information",
};

export default function whoami(): string {
  const session = getSession();

  return JSON.stringify(
    {
      userId: session.userId,
      loginIds: session.loginIds,
      permissions: session.permissions,
      roles: session.roles,
      tenants: session.tenants,
      expiresAt: session.expiresAt.toISOString(),
    },
    null,
    2,
  );
}
```

The `session` object contains the following fields:

* `session.userId`: The Descope user ID.
* `session.email`: Email address from the JWT claims.
* `session.token`: The raw bearer token from the request.
* `session.loginIds`: Login identifiers associated with the user (email, phone, etc.).
* `session.permissions`: Array of permissions granted to this session.
* `session.roles`: Roles assigned to the user.
* `session.tenants`: Tenant memberships, each with per-tenant `permissions` and `roles`.
* `session.expiresAt`: Token expiry as a `Date`.
* `session.issuedAt`: Token issue time as a `Date`.
* `session.claims`: Raw JWT claims object.

<Callout variant="warning">
  Do not call `getSession()` at module load time. Only call it inside tool handler functions where the middleware context is active.
</Callout>

### Example: Read custom JWT claims

Any claims added to Descope JWTs via [JWT templates](https://docs.descope.com/management/token/jwt-templates#custom-claims) are available on `session.claims`:

```typescript title="src/tools/custom-claims.ts"
import type { ToolMetadata } from "xmcp";
import { getSession } from "@xmcp-dev/descope";

export const metadata: ToolMetadata = {
  name: "custom-claims",
  description: "Returns custom claims from the authenticated user's JWT",
};

export default function customClaims(): string {
  const { claims } = getSession();

  const plan = claims["plan"] as string | undefined;
  const orgId = claims["org_id"] as string | undefined;

  return JSON.stringify({ plan, orgId }, null, 2);
}
```

`session.claims` is typed as `Record<string, unknown>`, so cast each value to the type you expect after reading it.

## Get user profile

Use `getUser()` to fetch full user details from the Descope Management API. This requires `managementKey` to be set in your provider configuration.

### Example: Return full user profile

```typescript title="src/tools/user-profile.ts"
import type { ToolMetadata } from "xmcp";
import { getUser } from "@xmcp-dev/descope";

export const metadata: ToolMetadata = {
  name: "user-profile",
  description: "Returns the full Descope user profile for the authenticated user",
};

export default async function userProfile(): Promise<string> {
  const user = await getUser();
  return JSON.stringify(user, null, 2);
}
```

## Access the SDK clients

### `getClient()`

Returns the full Descope Node SDK client, giving you access to all Descope features from within your tool handlers.

```typescript title="src/tools/advanced.ts"
import { getClient, getSession } from "@xmcp-dev/descope";

export default async function advanced(): Promise<string> {
  const client = getClient();
  const session = getSession();

  // Use any Descope SDK method
  const resp = await client.management.user.loadByUserId(session.userId);
  return JSON.stringify(resp.data, null, 2);
}
```

### `getManagementClient()`

Returns the `management` namespace of the Descope SDK. Requires `managementKey` to be configured.

```typescript title="src/tools/list-roles.ts"
import { getManagementClient } from "@xmcp-dev/descope";

export default async function listRoles(): Promise<string> {
  const mgmt = getManagementClient();
  const resp = await mgmt.role.loadAll();
  return JSON.stringify(resp.data, null, 2);
}
```

## Fetch a connection token

<Callout variant="info">
  Map your MCP server scopes to any corresponding connection scopes that are necessary to fetch from the Descope Connections Vault.
</Callout>

Use `fetchConnectionToken()` to retrieve a stored OAuth access token from a [Descope connection](https://docs.descope.com/agentic-identity-hub/core-components/connections). This uses the MCP Server's access token (no Management Key required).

```typescript title="src/tools/github-repos.ts"
import type { ToolMetadata } from "xmcp";
import { fetchConnectionToken } from "@xmcp-dev/descope";

export const metadata: ToolMetadata = {
  name: "github-repos",
  description: "Lists the authenticated user's GitHub repositories using a Descope connection token",
};

export default async function githubRepos(): Promise<string> {
  const accessToken = await fetchConnectionToken("github");

  const response = await fetch("https://api.github.com/user/repos", {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const repos = await response.json();
  return JSON.stringify(repos, null, 2);
}
```

## Troubleshooting

These are the most common errors you may encounter when using the Descope plugin:

* `unauthorized`: The request is missing the `Authorization` header. The MCP client must complete the OAuth flow before accessing protected routes.
* `token_expired`: The access token has expired. MCP clients should automatically refresh tokens; users can disconnect and reconnect to get fresh tokens.
* `invalid_token`: Token verification failed. Check that `DESCOPE_ISSUER_URL` contains the correct project ID that issued the token.

### OAuth Init Failed

If an MCP client fails to initialize the OAuth flow:

* Verify your resource exists in **Descope Console** → **Agentic Identity Hub** → **Resources**
* Confirm `DESCOPE_ISSUER_URL` matches the issuer URL shown in the console
* Ensure `BASE_URL` matches the base URL configured on your resource

### Session Not Initialized

If `getSession()` throws `"getSession() called outside of Descope middleware"`:

* Ensure `descopeProvider` is exported as default from `middleware.ts`
* Ensure the tool is called on a route under `/mcp/*`
* Do not call `getSession()` at module load time, only inside tool handlers

### Management Key Errors

If `getUser()` or `getManagementClient()` throws an error about the management key:

* Set `DESCOPE_MANAGEMENT_KEY` in your environment
* Pass `managementKey: process.env.DESCOPE_MANAGEMENT_KEY` in your `descopeProvider` config
* Verify the key is valid in **Descope Console** → **Company** → **Management Keys**

### Token Expired Errors

Access tokens are short-lived. If you see `token_expired` errors:

* MCP clients should automatically refresh tokens using the refresh token flow
* Users can disconnect and reconnect to get fresh tokens

### Invalid Token Errors

If token verification consistently fails:

* Verify `DESCOPE_ISSUER_URL` contains the correct project ID
* Ensure the MCP client is sending tokens issued by the correct Descope project
