Skip to main content
Glama

Okta MCP Server

users.ts29.6 kB
import { z } from "zod"; import pkg from "@okta/okta-sdk-nodejs"; const { Client: OktaClient } = pkg; // Schemas for input validation const userSchemas = { getUser: z.object({ userId: z.string().min(1, "User ID is required"), }), getUserByAttribute: z.object({ attribute: z.string().min(1, "Attribute name is required"), value: z.string().min(1, "Value is required"), operator: z.enum(["eq", "sw", "ew", "co", "pr"]).optional().default("eq"), limit: z.number().min(1).max(200).optional().default(50), includeInactive: z.boolean().optional().default(false), }), listUsers: z.object({ limit: z.number().min(1).max(200).optional().default(50), filter: z.string().optional(), search: z.string().optional(), after: z.string().optional(), sortBy: z.string().optional(), sortOrder: z.enum(["asc", "desc"]).optional().default("asc"), }), createUser: z.object({ firstName: z.string().min(1, "First name is required"), lastName: z.string().min(1, "Last name is required"), email: z.string().email("Valid email is required"), login: z.string().optional(), activate: z.boolean().optional().default(false), }), activateUser: z.object({ userId: z.string().min(1, "User ID is required"), sendEmail: z.boolean().optional().default(true), }), suspendUser: z.object({ userId: z.string().min(1, "User ID is required"), }), unsuspendUser: z.object({ userId: z.string().min(1, "User ID is required"), }), deactivateUser: z.object({ userId: z.string().min(1, "User ID is required"), }), deleteUser: z.object({ userId: z.string().min(1, "User ID is required"), }), getUserLastLocation: z.object({ userId: z.string().min(1, "User ID is required"), }), }; // Utility function to get Okta client function getOktaClient() { const oktaDomain = process.env.OKTA_ORG_URL; const apiToken = process.env.OKTA_API_TOKEN; if (!oktaDomain) { throw new Error( "OKTA_ORG_URL environment variable is not set. Please set it to your Okta domain." ); } if (!apiToken) { throw new Error( "OKTA_API_TOKEN environment variable is not set. Please generate an API token in the Okta Admin Console." ); } return new OktaClient({ orgUrl: oktaDomain, token: apiToken, }); } // Utility function to format profile values function getProfileValue(value: string | undefined | null): string { return value ?? "N/A"; } // Utility function to format dates function formatDate(dateString: Date | string | undefined | null): string { if (!dateString) return "N/A"; try { return new Date(dateString).toLocaleString(); } catch (e) { return dateString instanceof Date ? dateString.toISOString() : dateString || "N/A"; } } // Utility functions to handle PII safely function isPIIAttribute(attribute: string): boolean { const piiAttributes = [ "email", "login", "firstName", "lastName", "displayName", "nickName", "mobilePhone", "primaryPhone", "streetAddress", "secondEmail", "employeeNumber", "manager", "managerId", ]; return piiAttributes.includes(attribute.toLowerCase()); } function maskValue(value: string): string { if (value.length <= 3) { return "***"; } return ( value.charAt(0) + "*".repeat(value.length - 2) + value.charAt(value.length - 1) ); } // Tool definitions for users export const userTools = [ { name: "get_user", description: "Retrieve detailed user information from Okta by user ID", inputSchema: { type: "object", properties: { userId: { type: "string", description: "The unique identifier of the Okta user", }, }, required: ["userId"], }, }, { name: "find_users_by_attribute", description: "Search users by any profile attribute (manager, department, title, firstName, etc.)", inputSchema: { type: "object", properties: { attribute: { type: "string", description: "Profile attribute to search by. Common attributes: firstName, lastName, email, manager, department, title, division, organization, employeeNumber, costCenter, userType, city, state", }, value: { type: "string", description: "Value to search for", }, operator: { type: "string", description: "How to match the value: 'eq' (exact), 'sw' (starts with), 'ew' (ends with), 'co' (contains), 'pr' (has any value - ignores 'value' param)", enum: ["eq", "sw", "ew", "co", "pr"], }, limit: { type: "number", description: "Maximum users to return (default: 50, max: 200)", }, includeInactive: { type: "boolean", description: "Include suspended/deactivated users (default: false)", }, }, required: ["attribute", "value"], }, }, { name: "list_users", description: "List users from Okta with optional filtering and pagination", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Maximum number of users to return (default: 50, max: 200)", }, filter: { type: "string", description: "SCIM filter expression to filter users", }, search: { type: "string", description: "Free-form text search across multiple fields", }, after: { type: "string", description: "Cursor for pagination, obtained from previous response", }, sortBy: { type: "string", description: "Field to sort results by", }, sortOrder: { type: "string", description: "Sort order (asc or desc, default: asc)", enum: ["asc", "desc"], }, }, }, }, { name: "create_user", description: "Create a new user in Okta", inputSchema: { type: "object", properties: { firstName: { type: "string", description: "User's first name", }, lastName: { type: "string", description: "User's last name", }, email: { type: "string", description: "User's email address", }, login: { type: "string", description: "User's login (defaults to email if not provided)", }, activate: { type: "boolean", description: "Whether to activate the user immediately (default: false)", }, }, required: ["firstName", "lastName", "email"], }, }, { name: "activate_user", description: "Activate a user in Okta", inputSchema: { type: "object", properties: { userId: { type: "string", description: "The unique identifier of the Okta user", }, sendEmail: { type: "boolean", description: "Whether to send an activation email (default: true)", }, }, required: ["userId"], }, }, { name: "suspend_user", description: "Suspend a user in Okta", inputSchema: { type: "object", properties: { userId: { type: "string", description: "The unique identifier of the Okta user", }, }, required: ["userId"], }, }, { name: "unsuspend_user", description: "Unsuspend a user in Okta", inputSchema: { type: "object", properties: { userId: { type: "string", description: "The unique identifier of the Okta user", }, }, required: ["userId"], }, }, { name: "deactivate_user", description: "Deactivate a user in Okta", inputSchema: { type: "object", properties: { userId: { type: "string", description: "The unique identifier of the Okta user", }, }, required: ["userId"], }, }, { name: "delete_user", description: "Delete a user from Okta (must be deactivated first)", inputSchema: { type: "object", properties: { userId: { type: "string", description: "The unique identifier of the Okta user", }, }, required: ["userId"], }, }, { name: "get_user_last_location", description: "Retrieve the last known location and login information for a user from Okta system logs", inputSchema: { type: "object", properties: { userId: { type: "string", description: "The unique identifier of the Okta user", }, }, required: ["userId"], }, }, ]; // Handlers for user-related tools export const userHandlers = { get_user: async (request: { parameters: unknown }) => { const { userId } = userSchemas.getUser.parse(request.parameters); try { const oktaClient = getOktaClient(); const user = await oktaClient.userApi.getUser({ userId }); if (!user.profile) { throw new Error("User profile is undefined"); } const formattedUser = `• User Details: ID: ${user.id} Status: ${user.status} - Account Dates: Created: ${formatDate(user.created)} Activated: ${formatDate(user.activated)} Last Login: ${formatDate(user.lastLogin)} Last Updated: ${formatDate(user.lastUpdated)} Status Changed: ${formatDate(user.statusChanged)} Password Changed: ${formatDate(user.passwordChanged)} - Personal Information: Login: ${user.profile.login} Email: ${user.profile.email} Secondary Email: ${getProfileValue(user.profile.secondEmail)} First Name: ${user.profile.firstName} Last Name: ${user.profile.lastName} Display Name: ${user.profile.displayName} Nickname: ${getProfileValue(user.profile.nickName)} - Employment Details: Organization: ${user.profile.organization} Title: ${user.profile.title} Division: ${user.profile.division} Department: ${user.profile.department} Employee Number: ${user.profile.employeeNumber} User Type: ${user.profile.userType} Cost Center: ${user.profile.costCenter} Manager: ${getProfileValue(user.profile.manager)} ManagerId ${getProfileValue(user.profile.managerId)} - Contact Information: Mobile Phone: ${getProfileValue(user.profile.mobilePhone)} Primary Phone: ${getProfileValue(user.profile.primaryPhone)} - Address: Street: ${user.profile.streetAddress} City: ${user.profile.city} State: ${user.profile.state} Zip Code: ${user.profile.zipCode} Country: ${user.profile.countryCode} - Preferences: Preferred Language: ${user.profile.preferredLanguage} Profile URL: ${getProfileValue(user.profile.profileUrl)}`; return { content: [ { type: "text", text: formattedUser, }, ], }; } catch (error) { console.error("Error fetching user:", error); return { content: [ { type: "text", text: `Failed to get user: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, list_users: async (request: { parameters: unknown }) => { const params = userSchemas.listUsers.parse(request.parameters); try { // Build query parameters const queryParams: Record<string, any> = {}; if (params.limit) queryParams.limit = params.limit; if (params.after) queryParams.after = params.after; if (params.filter) queryParams.filter = params.filter; if (params.search) queryParams.search = params.search; if (params.sortBy) queryParams.sortBy = params.sortBy; if (params.sortOrder) queryParams.sortOrder = params.sortOrder; const oktaClient = getOktaClient(); // Get users list const users = await oktaClient.userApi.listUsers(queryParams); if (!users) { return { content: [ { type: "text", text: "No users data was returned from Okta.", }, ], }; } // Format the response let formattedResponse = "Users:\n"; let count = 0; // Track pagination info let after: string | undefined; // Process the users collection for await (const user of users) { // Check if user is valid if (!user || !user.id) { continue; } count++; // Remember the last user ID for pagination after = user.id; formattedResponse += ` ${count}. ${user.profile?.firstName || ""} ${user.profile?.lastName || ""} (${user.profile?.email || "No email"}) - ID: ${user.id} - Status: ${user.status || "Unknown"} - Created: ${formatDate(user.created)} - Last Updated: ${formatDate(user.lastUpdated)} `; } if (count === 0) { return { content: [ { type: "text", text: "No users found matching your criteria.", }, ], }; } // Add pagination information if (after && count >= (params.limit || 50)) { formattedResponse += `\nPagination:\n- Total users shown: ${count}\n`; formattedResponse += `- For next page, use 'after' parameter with value: ${after}\n`; } else { formattedResponse += `\nTotal users: ${count}\n`; } return { content: [ { type: "text", text: formattedResponse, }, ], }; } catch (error) { console.error("Error listing users:", error); return { content: [ { type: "text", text: `Failed to list users: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, create_user: async (request: { parameters: unknown }) => { const params = userSchemas.createUser.parse(request.parameters); try { const oktaClient = getOktaClient(); const newUser = { profile: { firstName: params.firstName, lastName: params.lastName, email: params.email, login: params.login || params.email, }, }; const user = await oktaClient.userApi.createUser({ body: newUser, activate: params.activate, }); return { content: [ { type: "text", text: `User created successfully: ID: ${user.id} Login: ${user.profile?.login} Status: ${user.status} Created: ${formatDate(user.created)}`, }, ], }; } catch (error) { console.error("Error creating user:", error); return { content: [ { type: "text", text: `Failed to create user: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, activate_user: async (request: { parameters: unknown }) => { const { userId, sendEmail } = userSchemas.activateUser.parse( request.parameters ); try { const oktaClient = getOktaClient(); await oktaClient.userApi.activateUser({ userId, sendEmail, }); return { content: [ { type: "text", text: `User with ID ${userId} has been activated successfully.${ sendEmail ? " An activation email has been sent." : "" }`, }, ], }; } catch (error) { console.error("Error activating user:", error); return { content: [ { type: "text", text: `Failed to activate user: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, suspend_user: async (request: { parameters: unknown }) => { const { userId } = userSchemas.suspendUser.parse(request.parameters); try { const oktaClient = getOktaClient(); await oktaClient.userApi.suspendUser({ userId, }); return { content: [ { type: "text", text: `User with ID ${userId} has been suspended.`, }, ], }; } catch (error) { console.error("Error suspending user:", error); return { content: [ { type: "text", text: `Failed to suspend user: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, unsuspend_user: async (request: { parameters: unknown }) => { const { userId } = userSchemas.unsuspendUser.parse(request.parameters); try { const oktaClient = getOktaClient(); await oktaClient.userApi.unsuspendUser({ userId, }); return { content: [ { type: "text", text: `User with ID ${userId} has been unsuspended and is now active.`, }, ], }; } catch (error) { console.error("Error unsuspending user:", error); return { content: [ { type: "text", text: `Failed to unsuspend user: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, deactivate_user: async (request: { parameters: unknown }) => { const { userId } = userSchemas.deactivateUser.parse(request.parameters); try { const oktaClient = getOktaClient(); await oktaClient.userApi.deactivateUser({ userId, }); return { content: [ { type: "text", text: `User with ID ${userId} has been deactivated.`, }, ], }; } catch (error) { console.error("Error deactivating user:", error); return { content: [ { type: "text", text: `Failed to deactivate user: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, delete_user: async (request: { parameters: unknown }) => { const { userId } = userSchemas.deleteUser.parse(request.parameters); try { const oktaClient = getOktaClient(); await oktaClient.userApi.deleteUser({ userId, }); return { content: [ { type: "text", text: `User with ID ${userId} has been permanently deleted.`, }, ], }; } catch (error) { console.error("Error deleting user:", error); return { content: [ { type: "text", text: `Failed to delete user: ${error instanceof Error ? error.message : String(error)}. Note: Users must be deactivated before they can be deleted.`, }, ], isError: true, }; } }, find_users_by_attribute: async (request: { parameters: unknown }) => { const { attribute, value, operator, limit, includeInactive } = userSchemas.getUserByAttribute.parse(request.parameters); try { const oktaClient = getOktaClient(); // Strategy 1: Try formatted search query first (much faster and more accurate) let users; let searchMethod = ""; try { // Construct proper search query based on attribute and operator let searchQuery; if (operator === "pr") { searchQuery = `profile.${attribute} pr`; } else { searchQuery = `profile.${attribute} ${operator} "${value}"`; } const searchParams = { search: searchQuery, limit: limit || 50, }; users = await oktaClient.userApi.listUsers(searchParams); searchMethod = "formatted search"; } catch (searchError) { // Check if it's an unsupported operator error const errorMessage = searchError instanceof Error ? searchError.message : String(searchError); if (errorMessage.includes("operator is not supported")) { // Strategy 2: Try free-text search (works for some attributes) try { const freeTextParams = { search: value, limit: limit || 50, }; users = await oktaClient.userApi.listUsers(freeTextParams); searchMethod = "free-text search"; } catch (freeTextError) { // Strategy 3: Fall back to client-side filtering const basicParams = { limit: 200, }; users = await oktaClient.userApi.listUsers(basicParams); searchMethod = "client-side filtering (limited to 200 users)"; } } else { // For other errors, go straight to client-side filtering const basicParams = { limit: 200, }; users = await oktaClient.userApi.listUsers(basicParams); searchMethod = "client-side filtering (limited to 200 users)"; } } if (!users) { return { content: [ { type: "text", text: "No users data returned from Okta.", }, ], }; } // Collect users and apply filtering const candidateUsers: any[] = []; for await (const user of users) { if (!user?.id) continue; candidateUsers.push(user); } // Apply filtering based on search method const matchingUsers: any[] = []; if (searchMethod === "formatted search") { // For formatted search, the results should already be filtered by Okta // We just need to apply status filtering and get full user details for (const user of candidateUsers) { try { const fullUser = await oktaClient.userApi.getUser({ userId: user.id, }); if (!fullUser?.profile) continue; // Apply status filter if (!includeInactive && fullUser.status !== "ACTIVE") continue; matchingUsers.push(fullUser); } catch (userError) { // Silent error handling - just skip this user continue; } } } else { // For free-text search and client-side filtering, we need to verify the attribute match for (const user of candidateUsers) { try { const fullUser = await oktaClient.userApi.getUser({ userId: user.id, }); if (!fullUser?.profile) continue; // Apply status filter if (!includeInactive && fullUser.status !== "ACTIVE") continue; // Check if this user actually matches our specific attribute const attributeValue = fullUser.profile[attribute as keyof typeof fullUser.profile]; // Handle 'pr' operator (present/exists) if (operator === "pr") { if ( attributeValue !== undefined && attributeValue !== null && attributeValue !== "" ) { matchingUsers.push(fullUser); } continue; } // For other operators, value must exist if (attributeValue === undefined || attributeValue === null) continue; const attrValueStr = String(attributeValue).toLowerCase(); const searchValueStr = value.toLowerCase(); let isMatch = false; switch (operator) { case "eq": isMatch = attrValueStr === searchValueStr; break; case "sw": isMatch = attrValueStr.startsWith(searchValueStr); break; case "ew": isMatch = attrValueStr.endsWith(searchValueStr); break; case "co": isMatch = attrValueStr.includes(searchValueStr); break; } if (isMatch) { matchingUsers.push(fullUser); if (matchingUsers.length >= (limit || 50)) { break; } } } catch (userError) { // Silent error handling - just skip this user continue; } } } // Build response - mask PII in search value display const displayValue = isPIIAttribute(attribute) ? maskValue(value) : value; let response = `Search Results for ${attribute} ${operator} "${displayValue}"\n`; response += `Method: ${searchMethod}\n`; response += `${includeInactive ? "Including" : "Excluding"} inactive users\n\n`; if (matchingUsers.length === 0) { response += `No users found with ${attribute} matching the specified criteria`; if (searchMethod.includes("limited")) { response += `\n(Note: Only checked first 200 users for performance)`; } } else { const userList: string[] = []; matchingUsers.forEach((user, index) => { const actualValue = user.profile?.[attribute as keyof typeof user.profile] || "N/A"; userList.push(`${index + 1}. ${user.profile?.firstName || ""} ${user.profile?.lastName || ""} Email: ${user.profile?.email || "No email"} ID: ${user.id} Status: ${user.status} ${attribute}: ${actualValue} Department: ${user.profile?.department || "N/A"} Title: ${user.profile?.title || "N/A"} Last Login: ${formatDate(user.lastLogin)}`); }); response += userList.join("\n\n"); response += `\n\nTotal found: ${matchingUsers.length} user${matchingUsers.length !== 1 ? "s" : ""}`; // Add helpful note about search capabilities if (searchMethod === "formatted search") { response += `\n\n✅ Note: This search used Okta's native filtering for optimal performance.`; } else if (searchMethod.includes("limited")) { response += `\n\n⚠️ Note: This search was limited to 200 users for performance. For comprehensive results across all users, consider using more specific search criteria.`; } } return { content: [ { type: "text", text: response, }, ], }; } catch (error) { console.error("[ERROR] Search failed:", error); return { content: [ { type: "text", text: `Search failed: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, get_user_last_location: async (request: { parameters: unknown }) => { const { userId } = userSchemas.getUserLastLocation.parse( request.parameters ); try { const oktaClient = getOktaClient(); // First get the user to ensure they exist and get their login const user = await oktaClient.userApi.getUser({ userId }); if (!user || !user.profile) { return { content: [ { type: "text", text: `User with ID ${userId} not found.`, }, ], }; } // Get the last 90 days of system logs for this user's login events const ninetyDaysAgo = new Date(); ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); // Use the system log API to get login events const logs = await oktaClient.systemLogApi.listLogEvents({ since: ninetyDaysAgo.toISOString(), filter: `target.id eq "${userId}" and (eventType eq "user.session.start" or eventType eq "user.authentication.auth_via_mfa" or eventType eq "user.authentication.sso")`, limit: 1, }); // Get the first (most recent) log entry const lastLogin = await logs.next(); if (!lastLogin || !lastLogin.value) { return { content: [ { type: "text", text: `No login events found for user ${user.profile.login} in the last 90 days. This might mean the user hasn't logged in recently or the events are not being captured in the system logs.`, }, ], }; } const event = lastLogin.value; const clientData = event.client || {}; const geographicalContext = event.client?.geographicalContext || {}; const formattedLocation = `• Last Login Information for User ${user.profile.login}: Time: ${formatDate(event.published)} Event Type: ${event.eventType || "N/A"} IP Address: ${clientData.ipAddress || "N/A"} City: ${geographicalContext.city || "N/A"} State: ${geographicalContext.state || "N/A"} Country: ${geographicalContext.country || "N/A"} Device: ${clientData.device || "N/A"} User Agent: ${clientData.userAgent || "N/A"}`; return { content: [ { type: "text", text: formattedLocation, }, ], }; } catch (error) { console.error("Error fetching user location:", error); return { content: [ { type: "text", text: `Failed to fetch user location: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }, };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kapilduraphe/okta-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server