Descope

The Descope plugin provides authentication for your MCP server using Descope's OAuth 2.1 agentic identity system with Dynamic Client Registration.

For the complete documentation index, see llms.txt. Markdown variants of every page are available by appending .md to the URL.

Installation

Install the Descope plugin:

pnpm i @xmcp-dev/descope

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 ConsoleAgentic Identity HubResources
  2. Click Create ResourceMCP 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 ConsoleProject Settings).

Environment Variables

Configure the following environment variables in your .env file:

Code
# 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"

Create a Management Key (optional)

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 ConsoleCompanyManagement Keys
  2. Click + Management Key
  3. Copy and save the key to your .env file since it is only shown once
Code
# 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:

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:

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

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.

Example: Read custom JWT claims

Any claims added to Descope JWTs via JWT templates are available on session.claims:

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

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.

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.

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

Use fetchConnectionToken() to retrieve a stored OAuth access token from a Descope connection. This uses the MCP Server's access token (no Management Key required).

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 ConsoleAgentic Identity HubResources
  • 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 ConsoleCompanyManagement 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