descope-mcp-server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import dotenv from "dotenv";
import DescopeClient from '@descope/node-sdk';
dotenv.config();
const DESCOPE_PROJECT_ID = process.env.DESCOPE_PROJECT_ID;
const DESCOPE_MANAGEMENT_KEY = process.env.DESCOPE_MANAGEMENT_KEY;
if (!DESCOPE_PROJECT_ID || !DESCOPE_MANAGEMENT_KEY) {
throw new Error('DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY must be set');
}
const descope = DescopeClient({
projectId: DESCOPE_PROJECT_ID,
managementKey: DESCOPE_MANAGEMENT_KEY,
});
export const createServer = () => {
// Create server instance
const server = new McpServer({
name: "descope",
version: "1.0.0",
});
// Register Descope tools
// Add search-audits tool
server.tool(
"search-audits",
"Search Descope project audit logs",
{
// Optional filters
loginIds: z.array(z.string()).optional()
.describe("Filter by specific login IDs"),
actions: z.array(z.string()).optional()
.describe("Filter by specific action types"),
excludedActions: z.array(z.string()).optional()
.describe("Actions to exclude from results"),
tenants: z.array(z.string()).optional()
.describe("Filter by specific tenant IDs"),
noTenants: z.boolean().optional()
.describe("If true, only show events without tenants"),
methods: z.array(z.string()).optional()
.describe("Filter by authentication methods"),
geos: z.array(z.string()).optional()
.describe("Filter by geographic locations"),
// Time range (defaults to last 24 hours)
hoursBack: z.number().min(1).max(24 * 30).default(24)
.describe("Hours to look back (max 720 hours / 30 days)"),
// Limit (defaults to 5)
limit: z.number().min(1).max(10).default(5)
.describe("Number of audit logs to fetch (max 10)"),
},
async ({ loginIds, actions, excludedActions, tenants, noTenants, methods, geos, hoursBack, limit }) => {
try {
const now = Date.now();
const from = now - (hoursBack * 60 * 60 * 1000);
const audits = await descope.management.audit.search({
from,
to: now,
loginIds,
actions,
excludedActions,
tenants,
noTenants,
methods,
geos,
});
// Limit the number of audits to the specified limit
const auditResponse = audits.data;
const limitedAudits = auditResponse ? auditResponse.slice(0, limit) : [];
return {
content: [
{
type: "text",
text: `Audit logs for the last ${hoursBack} hours:\n\n${JSON.stringify(limitedAudits, null, 2)}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching audit logs: ${error}`,
},
],
};
}
},
);
// Add search-users tool
server.tool(
"search-users",
"Search for users in Descope project",
{
// Search parameters
text: z.string().optional()
.describe("Text to search for in user fields"),
emails: z.array(z.string()).optional()
.describe("Filter by specific email addresses"),
phones: z.array(z.string()).optional()
.describe("Filter by specific phone numbers"),
statuses: z.array(z.enum(['enabled', 'disabled', 'invited'])).optional()
.describe("Filter by user statuses ('enabled', 'disabled', or 'invited')"),
roles: z.array(z.string()).optional()
.describe("Filter users by role names"),
tenantIds: z.array(z.string()).optional()
.describe("Filter users by specific tenant IDs"),
ssoAppIds: z.array(z.string()).optional()
.describe("Filter users by SSO application IDs"),
loginIds: z.array(z.string()).optional()
.describe("Filter by specific login IDs"),
withTestUser: z.boolean().optional()
.describe("Include test users in results"),
testUsersOnly: z.boolean().optional()
.describe("Return only test users"),
page: z.number().min(0).optional()
.describe("Page number for pagination"),
limit: z.number().min(1).max(100).default(10)
.describe("Number of users per page (max 100)"),
},
async ({ text, emails, phones, statuses, roles, tenantIds, ssoAppIds, loginIds, withTestUser, testUsersOnly, page, limit }) => {
try {
const users = await descope.management.user.search({
text,
emails,
phones,
statuses,
roles,
tenantIds,
ssoAppIds,
loginIds,
withTestUser,
testUsersOnly,
page,
limit,
});
return {
content: [
{
type: "text",
text: `Found users:\n\n${JSON.stringify(users.data, null, 2)}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error searching users: ${error}`,
},
],
};
}
},
);
// Add create-user tool
server.tool(
"create-user",
"Create a new user in Descope project",
{
loginId: z.string()
.describe("Primary login identifier for the user"),
additionalLoginIds: z.array(z.string()).optional()
.describe("Additional login identifiers"),
email: z.string().email().optional()
.describe("User's email address"),
verifiedEmail: z.boolean().optional()
.describe("Whether the email is pre-verified"),
phone: z.string().optional()
.describe("User's phone number in E.164 format"),
verifiedPhone: z.boolean().optional()
.describe("Whether the phone is pre-verified"),
displayName: z.string().optional()
.describe("User's display name"),
givenName: z.string().optional()
.describe("User's given/first name"),
middleName: z.string().optional()
.describe("User's middle name"),
familyName: z.string().optional()
.describe("User's family/last name"),
picture: z.string().url().optional()
.describe("URL to user's profile picture"),
roles: z.array(z.string()).optional()
.describe("Global role names to assign to the user"),
userTenants: z.array(z.object({
tenantId: z.string(),
roleNames: z.array(z.string()),
})).optional()
.describe("Tenant associations with specific roles"),
ssoAppIds: z.array(z.string()).optional()
.describe("SSO application IDs to associate"),
customAttributes: z.record(z.any()).optional()
.describe("Custom attributes for the user"),
},
async ({ loginId, ...options }) => {
try {
const user = await descope.management.user.create(loginId, options);
return {
content: [
{
type: "text",
text: `Successfully created user:\n\n${JSON.stringify(user.data, null, 2)}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating user: ${error}`,
},
],
};
}
},
);
// Add invite-user tool
server.tool(
"invite-user",
"Create and invite a new user to the Descope project",
{
// Basic user info
loginId: z.string()
.describe("Primary login identifier for the user"),
additionalLoginIds: z.array(z.string()).optional()
.describe("Additional login identifiers"),
email: z.string().email().optional()
.describe("User's email address"),
verifiedEmail: z.boolean().optional()
.describe("Whether the email is pre-verified"),
phone: z.string().optional()
.describe("User's phone number in E.164 format"),
verifiedPhone: z.boolean().optional()
.describe("Whether the phone is pre-verified"),
displayName: z.string().optional()
.describe("User's display name"),
givenName: z.string().optional()
.describe("User's given/first name"),
middleName: z.string().optional()
.describe("User's middle name"),
familyName: z.string().optional()
.describe("User's family/last name"),
picture: z.string().url().optional()
.describe("URL to user's profile picture"),
roles: z.array(z.string()).optional()
.describe("Global role names to assign to the user"),
userTenants: z.array(z.object({
tenantId: z.string(),
roleNames: z.array(z.string()),
})).optional()
.describe("Tenant associations with specific roles"),
ssoAppIds: z.array(z.string()).optional()
.describe("SSO application IDs to associate"),
customAttributes: z.record(z.any()).optional()
.describe("Custom attributes for the user"),
// Invite specific options
inviteUrl: z.string().url().optional()
.describe("Custom URL for the invitation link"),
sendMail: z.boolean().optional()
.describe("Send invite via email (default follows project settings)"),
sendSMS: z.boolean().optional()
.describe("Send invite via SMS (default follows project settings)"),
templateId: z.string().optional()
.describe("Custom template ID for the invitation"),
templateOptions: z.object({
appUrl: z.string().url().optional()
.describe("Application URL to use in the template"),
redirectUrl: z.string().url().optional()
.describe("URL to redirect after authentication"),
customClaims: z.string().optional()
.describe("Custom claims to include in the template (as JSON string)"),
}).optional()
.describe("Options for customizing the invitation template"),
},
async ({ loginId, inviteUrl, sendMail, sendSMS, templateId, templateOptions, ...userOptions }) => {
try {
// Define the type for invite options
const inviteOptions: {
inviteUrl?: string;
sendMail?: boolean;
sendSMS?: boolean;
templateId?: string;
templateOptions?: {
appUrl?: string;
redirectUrl?: string;
customClaims?: string;
};
} & typeof userOptions = {
...userOptions,
inviteUrl,
sendMail,
sendSMS,
templateId,
};
// Only add templateOptions if they exist and ensure customClaims is handled properly
if (templateOptions) {
inviteOptions.templateOptions = {
appUrl: templateOptions.appUrl,
redirectUrl: templateOptions.redirectUrl,
};
// Only add customClaims if it's provided
if (templateOptions.customClaims) {
inviteOptions.templateOptions.customClaims = templateOptions.customClaims;
}
}
const user = await descope.management.user.invite(loginId, inviteOptions);
return {
content: [
{
type: "text",
text: `Successfully invited user:\n\n${JSON.stringify(user.data, null, 2)}`,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error inviting user: ${error}`,
},
],
};
}
},
);
return { server };
}