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/descopeDescope 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
- Go to Descope Console → Agentic Identity Hub → Resources
- Click Create Resource → MCP Server
- Set a name and your server's base URL (e.g.,
http://127.0.0.1:3001for local development) - 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:
# 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.
- Go to Descope Console → Company → Management Keys
- Click + Management Key
- Copy and save the key to your
.envfile since it is only shown once
# Descope Project Management Key
DESCOPE_MANAGEMENT_KEY=your-management-keySet up the Provider
Create a middleware.ts file in your xmcp app's src directory:
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:
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 ofissuerURL.managementKey: (Optional) Descope management key. Required to usegetUser()orgetManagementClient().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
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-tenantpermissionsandroles.session.expiresAt: Token expiry as aDate.session.issuedAt: Token issue time as aDate.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:
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
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.
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.
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).
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 theAuthorizationheader. 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 thatDESCOPE_ISSUER_URLcontains 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_URLmatches the issuer URL shown in the console - Ensure
BASE_URLmatches the base URL configured on your resource
Session Not Initialized
If getSession() throws "getSession() called outside of Descope middleware":
- Ensure
descopeProvideris exported as default frommiddleware.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_KEYin your environment - Pass
managementKey: process.env.DESCOPE_MANAGEMENT_KEYin yourdescopeProviderconfig - 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_URLcontains the correct project ID - Ensure the MCP client is sending tokens issued by the correct Descope project