Okta MCP Server
by kapilduraphe
Verified
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { User, UserApiGetUserRequest } from "@okta/okta-sdk-nodejs";
import pkg from "@okta/okta-sdk-nodejs";
const { Client: OktaClient } = pkg;
import { z } from "zod";
// Initialize the server
const server = new Server(
{
name: "okta-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Schema definitions for input validation
const schemas = {
toolInputs: {
getUser: z.object({
userId: z.string().min(1, "User ID is required"),
}),
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"),
}),
listGroups: 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"),
}),
},
};
// Interface definitions for Okta data structures
interface OktaUserProfile {
login: string;
email: string;
secondEmail?: string;
firstName: string;
lastName: string;
displayName: string;
nickName?: string;
organization: string;
title: string;
division: string;
department: string;
employeeNumber: string;
userType: string;
costCenter: string;
mobilePhone?: string;
primaryPhone?: string;
streetAddress: string;
city: string;
state: string;
zipCode: string;
countryCode: string;
preferredLanguage: string;
profileUrl?: string;
}
interface OktaUser {
id: string;
status: string;
created: string;
activated: string;
lastLogin: string;
lastUpdated: string;
statusChanged: string;
passwordChanged: string;
profile: OktaUserProfile;
}
interface OktaGroupProfile {
name: string;
description?: string;
}
interface OktaGroup {
id: string;
created?: string;
lastUpdated?: string;
lastMembershipUpdated?: string;
type?: string;
objectClass?: string[];
profile?: OktaGroupProfile;
}
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,
});
}
// Tool definitions
const TOOL_DEFINITIONS = [
{
name: "get_user",
description:
"Retrieve detailed user information from Okta by user ID, including profile, account status, dates, employment details, and contact information",
inputSchema: {
type: "object",
properties: {
userId: {
type: "string",
description: "The unique identifier of the Okta user",
},
},
required: ["userId"],
},
},
{
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 (e.g. 'profile.firstName eq \"John\"')",
},
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 (e.g. 'status', 'created', 'lastUpdated')",
},
sortOrder: {
type: "string",
description: "Sort order (asc or desc, default: asc)",
enum: ["asc", "desc"],
},
},
},
},
{
name: "list_groups",
description:
"List user groups from Okta with optional filtering and pagination",
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description:
"Maximum number of groups to return (default: 50, max: 200)",
},
filter: {
type: "string",
description:
"Filter expression for groups (e.g. 'type eq \"OKTA_GROUP\"')",
},
search: {
type: "string",
description: "Free-form text search across group fields",
},
after: {
type: "string",
description: "Cursor for pagination, obtained from previous response",
},
sortBy: {
type: "string",
description: "Field to sort results by (e.g. 'name', 'type')",
},
sortOrder: {
type: "string",
description: "Sort order (asc or desc, default: asc)",
enum: ["asc", "desc"],
},
},
},
},
];
// Types
interface OktaApiError {
status?: number;
message: string;
}
type ToolHandler = (args: unknown) => Promise<{
content: Array<{ type: "text"; text: string }>;
}>;
// Utility functions
function isOktaApiError(error: any): error is OktaApiError {
return error !== null && typeof error === "object" && "status" in error;
}
function getProfileValue(value: string | undefined | null): string {
return value ?? "N/A";
}
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";
}
}
function formatArray(arr: string[] | undefined | null): string {
if (!arr || arr.length === 0) return "N/A";
return arr.join(", ");
}
// Tool handlers
const toolHandlers: Record<string, ToolHandler> = {
get_user: async (args: unknown) => {
const { userId } = schemas.toolInputs.getUser.parse(args);
try {
const params: UserApiGetUserRequest = {
userId,
};
const oktaClient = getOktaClient();
const user = await oktaClient.userApi.getUser(params);
if (!user.profile) {
throw new Error("User profile is undefined");
}
const formattedUser = `• User Details:
ID: ${user.id}
Status: ${user.status}
- Account Dates:
Created: ${user.created}
Activated: ${user.activated}
Last Login: ${user.lastLogin}
Last Updated: ${user.lastUpdated}
Status Changed: ${user.statusChanged}
Password Changed: ${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}
- 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" as const,
text: formattedUser,
},
],
};
} catch (error) {
if (isOktaApiError(error) && error.status === 404) {
return {
content: [
{
type: "text" as const,
text: `User with ID ${userId} not found.`,
},
],
};
}
console.error("Error fetching user:", error);
throw new Error(
`Failed to fetch user details: ${error instanceof Error ? error.message : String(error)}`
);
}
},
list_users: async (args: unknown) => {
const params = schemas.toolInputs.listUsers.parse(args);
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" as const,
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" as const,
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" as const,
text: formattedResponse,
},
],
};
} catch (error) {
console.error("Error listing users:", error);
throw new Error(
`Failed to list users: ${error instanceof Error ? error.message : String(error)}`
);
}
},
list_groups: async (args: unknown) => {
const params = schemas.toolInputs.listGroups.parse(args);
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 groups list
const groups = await oktaClient.groupApi.listGroups(queryParams);
if (!groups) {
return {
content: [
{
type: "text" as const,
text: "No groups data was returned from Okta.",
},
],
};
}
// Format the response
let formattedResponse = "Groups:\n";
let count = 0;
// Track pagination info
let after: string | undefined;
// Process the groups collection
for await (const group of groups) {
// Check if group is valid
if (!group || !group.id) {
continue;
}
count++;
// Remember the last group ID for pagination
after = group.id;
formattedResponse += `
${count}. ${group.profile?.name || "Unnamed Group"}
- ID: ${group.id}
- Type: ${group.type || "Unknown"}
- Object Class: ${formatArray(group.objectClass)}
- Description: ${group.profile?.description || "No description"}
- Created: ${formatDate(group.created)}
- Last Updated: ${formatDate(group.lastUpdated)}
- Last Membership Updated: ${formatDate(group.lastMembershipUpdated)}
`;
}
if (count === 0) {
return {
content: [
{
type: "text" as const,
text: "No groups found matching your criteria.",
},
],
};
}
// Add pagination information
if (after && count >= (params.limit || 50)) {
formattedResponse += `\nPagination:\n- Total groups shown: ${count}\n`;
formattedResponse += `- For next page, use 'after' parameter with value: ${after}\n`;
} else {
formattedResponse += `\nTotal groups: ${count}\n`;
}
return {
content: [
{
type: "text" as const,
text: formattedResponse,
},
],
};
} catch (error) {
console.error("Error listing groups:", error);
throw new Error(
`Failed to list groups: ${error instanceof Error ? error.message : String(error)}`
);
}
},
}; // Added closing brace for toolHandlers object
// Register tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOL_DEFINITIONS };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const handler = toolHandlers[name as keyof typeof toolHandlers];
if (!handler) {
throw new Error(`Unknown tool: ${name}`);
}
return await handler(args);
} catch (error) {
console.error(`Error executing tool ${name}:`, error);
throw new Error(
`Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`
);
}
});
// Start the server
async function main() {
try {
const transport = new StdioServerTransport();
await server.connect(transport);
} catch (error) {
console.error(
"Startup error:",
error instanceof Error ? error.message : String(error)
);
process.exit(1);
}
}
main().catch((error) => {
console.error(
"Fatal error in main():",
error instanceof Error ? error.message : String(error)
);
process.exit(1);
});