Skip to main content
Glama

Advanced Keycloak MCP server

by Octodet
index.ts19.4 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js"; import KcAdminClient from "@keycloak/keycloak-admin-client"; import { z } from "zod"; import pkg from '../package.json' with { type: 'json' }; // Import version from package.json export const VERSION = pkg.version; // Types interface KeycloakConfig { baseUrl: string; adminUsername: string; adminPassword: string; } // Configuration schema with validation const ConfigSchema = z.object({ baseUrl: z .string() .trim() .min(1, "Keycloak URL cannot be empty") .refine( (url) => { try { new URL(url); return true; } catch { return url.startsWith('http://') || url.startsWith('https://'); } }, "Keycloak URL must be a valid URL starting with http:// or https://" ) .transform((url) => url.replace(/\/+$/, "")) // Remove trailing slashes .describe("Keycloak server URL"), adminUsername: z .string() .trim() .min(1, "Admin username cannot be empty") .describe("Keycloak admin username"), adminPassword: z .string() .trim() .min(1, "Admin password cannot be empty") .describe("Keycloak admin password"), }); // Keycloak Service class class KeycloakService { private config: KeycloakConfig; private client: KcAdminClient; private isAuthenticated: boolean = false; private authTokenExpiry: number = 0; constructor(config: KeycloakConfig) { this.config = ConfigSchema.parse(config); this.client = new KcAdminClient({ baseUrl: this.config.baseUrl, realmName: "master", }); } private async authenticate(): Promise<void> { // Check if we have a valid token const now = Date.now(); if (this.isAuthenticated && now < this.authTokenExpiry) { return; } try { // Set the realm for authentication (usually master for admin operations) this.client.setConfig({ realmName: "master" }); const authResult = await this.client.auth({ username: this.config.adminUsername, password: this.config.adminPassword, grantType: "password", clientId: "admin-cli", }); this.isAuthenticated = true; // Set token expiry to 5 minutes from now (tokens typically last longer, but this is safe) this.authTokenExpiry = now + (5 * 60 * 1000); } catch (error) { this.isAuthenticated = false; this.authTokenExpiry = 0; throw new McpError( ErrorCode.InternalError, `Failed to authenticate with Keycloak: ${error instanceof Error ? error.message : String(error)}` ); } } async createUser(params: { realm: string; username: string; email: string; firstName: string; lastName: string; enabled?: boolean; emailVerified?: boolean; credentials?: Array<{ type: string; value: string; temporary?: boolean; }>; }) { await this.authenticate(); this.client.setConfig({ realmName: params.realm }); const user = await this.client.users.create({ realm: params.realm, username: params.username, email: params.email, firstName: params.firstName, lastName: params.lastName, enabled: params.enabled !== undefined ? params.enabled : true, emailVerified: params.emailVerified, credentials: params.credentials, }); return user; } async deleteUser(realm: string, userId: string) { await this.authenticate(); this.client.setConfig({ realmName: realm }); await this.client.users.del({ id: userId, realm, }); } async listRealms() { await this.authenticate(); return await this.client.realms.find(); } async listUsers(realm: string) { await this.authenticate(); this.client.setConfig({ realmName: realm }); return await this.client.users.find(); } async listRoles(realm: string, clientId: string) { await this.authenticate(); this.client.setConfig({ realmName: realm }); // Find the client by clientId (can be id or clientId string) let client = null; try { client = await this.client.clients.findOne({ realm, id: clientId }); } catch {} if (!client) { const clients = await this.client.clients.find({ realm }); client = clients.find( (c) => c.clientId === clientId || c.id === clientId ); } if (!client) { throw new McpError( ErrorCode.InvalidRequest, `Client '${clientId}' not found in realm '${realm}'.` ); } if (!client.id || typeof client.id !== "string") { throw new McpError( ErrorCode.InvalidRequest, `Client found but has no valid id property.` ); } const roles = await this.client.clients.listRoles({ realm, id: client.id, }); return { client, roles }; } async updateUserRoles(params: { realm: string; userId: string; clientId: string; rolesToAdd?: string[]; rolesToRemove?: string[]; }) { await this.authenticate(); this.client.setConfig({ realmName: params.realm }); let added: string[] = []; let removed: string[] = []; let errors: string[] = []; // Find the client let client = null; try { client = await this.client.clients.findOne({ realm: params.realm, id: params.clientId }); } catch {} if (!client) { const clients = await this.client.clients.find({ realm: params.realm }); client = clients.find( (c) => c.clientId === params.clientId || c.id === params.clientId ); } if (!client || !client.id || typeof client.id !== "string") { throw new McpError( ErrorCode.InvalidRequest, `Client '${params.clientId}' not found or invalid in realm '${params.realm}'.` ); } // Fetch all roles for this client const allRoles = await this.client.clients.listRoles({ realm: params.realm, id: client.id, }); const nameToRole = Object.fromEntries(allRoles.map((r) => [r.name, r])); // Add roles if (params.rolesToAdd && params.rolesToAdd.length > 0) { const addObjs = params.rolesToAdd .map((name) => nameToRole[name]) .filter(Boolean); if (addObjs.length !== params.rolesToAdd.length) { errors.push("Some roles to add not found"); } if (addObjs.length > 0) { await this.client.users.addClientRoleMappings({ id: params.userId, realm: params.realm, clientUniqueId: client.id, roles: addObjs, }); added = addObjs.map((r) => r.name!); } } // Remove roles if (params.rolesToRemove && params.rolesToRemove.length > 0) { const removeObjs = params.rolesToRemove .map((name) => nameToRole[name]) .filter(Boolean); if (removeObjs.length !== params.rolesToRemove.length) { errors.push("Some roles to remove not found"); } if (removeObjs.length > 0) { await this.client.users.delClientRoleMappings({ id: params.userId, realm: params.realm, clientUniqueId: client.id, roles: removeObjs, }); removed = removeObjs.map((r) => r.name!); } } return { client, added, removed, errors }; } async resetUserPassword(params: { realm: string; userId: string; password: string; temporary?: boolean; }) { await this.authenticate(); this.client.setConfig({ realmName: params.realm }); await this.client.users.resetPassword({ id: params.userId, realm: params.realm, credential: { type: "password", value: params.password, temporary: params.temporary || false, }, }); } } // Function to create and configure the MCP server export async function createKeycloakMcpServer(config: KeycloakConfig): Promise<Server> { let validatedConfig; try { validatedConfig = ConfigSchema.parse(config); } catch (error) { throw new McpError( ErrorCode.InvalidRequest, `Invalid configuration: ${error instanceof Error ? error.message : String(error)}` ); } // Create Keycloak service instance const keycloakService = new KeycloakService(validatedConfig); // Create server instance const server = new Server( { name: "@octodet/keycloak-mcp", version: VERSION, }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "create-user", description: "Create a new user in a specific realm", inputSchema: { type: "object", properties: { realm: { type: "string", description: "Realm name" }, username: { type: "string", description: "Username for the new user" }, email: { type: "string", format: "email", description: "Email address for the new user" }, firstName: { type: "string", description: "First name of the user" }, lastName: { type: "string", description: "Last name of the user" }, enabled: { type: "boolean", description: "Whether the user is enabled", default: true }, emailVerified: { type: "boolean", description: "Whether the email is verified" }, credentials: { type: "array", items: { type: "object", properties: { type: { type: "string", description: "Credential type (e.g., 'password')" }, value: { type: "string", description: "Credential value" }, temporary: { type: "boolean", description: "Whether the credential is temporary" }, }, required: ["type", "value"], }, description: "User credentials", }, }, required: ["realm", "username", "email", "firstName", "lastName"], }, }, { name: "delete-user", description: "Delete a user from a specific realm", inputSchema: { type: "object", properties: { realm: { type: "string", description: "Realm name" }, userId: { type: "string", description: "User ID to delete" }, }, required: ["realm", "userId"], }, }, { name: "list-realms", description: "List all available realms", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "list-users", description: "List users in a specific realm", inputSchema: { type: "object", properties: { realm: { type: "string", description: "Realm name" }, }, required: ["realm"], }, }, { name: "list-roles", description: "List all roles of a specific client in a specific realm", inputSchema: { type: "object", properties: { realm: { type: "string", description: "Realm name" }, clientId: { type: "string", description: "Client ID" }, }, required: ["realm", "clientId"], }, }, { name: "update-user-roles", description: "Add and/or remove client roles for a user in a specific realm and client", inputSchema: { type: "object", properties: { realm: { type: "string", description: "Realm name" }, userId: { type: "string", description: "User ID" }, clientId: { type: "string", description: "Client ID" }, rolesToAdd: { type: "array", items: { type: "string" }, description: "Roles to add" }, rolesToRemove: { type: "array", items: { type: "string" }, description: "Roles to remove" }, }, required: ["realm", "userId", "clientId"], }, }, { name: "reset-user-password", description: "Reset or set a new password for a user in a specific realm", inputSchema: { type: "object", properties: { realm: { type: "string", description: "Realm name" }, userId: { type: "string", description: "User ID" }, password: { type: "string", description: "New password" }, temporary: { type: "boolean", description: "Whether the password is temporary", default: false }, }, required: ["realm", "userId", "password"], }, }, ], }; }); // Tool schemas for validation const CreateUserSchema = z.object({ realm: z.string(), username: z.string(), email: z.string().email(), firstName: z.string(), lastName: z.string(), enabled: z.boolean().default(true), emailVerified: z.boolean().optional(), credentials: z .array( z.object({ type: z.string(), value: z.string(), temporary: z.boolean().optional(), }) ) .optional(), }); const DeleteUserSchema = z.object({ realm: z.string(), userId: z.string(), }); const ListUsersSchema = z.object({ realm: z.string(), }); const ListRolesSchema = z.object({ realm: z.string(), clientId: z.string(), }); const UpdateUserRolesSchema = z.object({ realm: z.string(), userId: z.string(), clientId: z.string(), rolesToAdd: z.array(z.string()).optional(), rolesToRemove: z.array(z.string()).optional(), }); const ResetUserPasswordSchema = z.object({ realm: z.string(), userId: z.string(), password: z.string(), temporary: z.boolean().default(false), }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "create-user": { const params = CreateUserSchema.parse(args); const user = await keycloakService.createUser(params); return { content: [ { type: "text", text: `User created successfully. User ID: ${user.id}`, }, ], }; } case "delete-user": { const { realm, userId } = DeleteUserSchema.parse(args); await keycloakService.deleteUser(realm, userId); return { content: [ { type: "text", text: `User ${userId} deleted successfully from realm ${realm}`, }, ], }; } case "list-realms": { const realms = await keycloakService.listRealms(); return { content: [ { type: "text", text: `Available realms:\n${realms .map((r) => `- ${r.realm}`) .join("\n")}`, }, ], }; } case "list-users": { const { realm } = ListUsersSchema.parse(args); const users = await keycloakService.listUsers(realm); return { content: [ { type: "text", text: `Users in realm ${realm}:\n${users .map((u) => `- ${u.username} (${u.id})`) .join("\n")}`, }, ], }; } case "list-roles": { const { realm, clientId } = ListRolesSchema.parse(args); const { client, roles } = await keycloakService.listRoles(realm, clientId); return { content: [ { type: "text", text: `Roles for client '${client.clientId}' in realm '${realm}':\n${roles .map((r) => `- ${r.name}`) .join("\n")}`, }, ], }; } case "update-user-roles": { const params = UpdateUserRolesSchema.parse(args); const { client, added, removed, errors } = await keycloakService.updateUserRoles(params); return { content: [ { type: "text", text: `Client roles updated for user ${params.userId} in realm ${params.realm} (client: ${ client.clientId }).\nAdded: ${added.join(", ") || "none"}\nRemoved: ${ removed.join(", ") || "none" }${errors.length ? `\nErrors: ${errors.join(", ")}` : ""}`, }, ], }; } case "reset-user-password": { const params = ResetUserPasswordSchema.parse(args); await keycloakService.resetUserPassword(params); return { content: [ { type: "text", text: `Password ${params.temporary ? "temporarily " : ""}reset successfully for user ${params.userId} in realm ${params.realm}${ params.temporary ? ". User will be required to change password on next login." : "." }`, }, ], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { if (error instanceof z.ZodError) { return { isError: true, content: [ { type: "text", text: `Invalid arguments: ${error.errors .map((e) => `${e.path.join(".")}: ${e.message}`) .join(", ")}`, }, ], }; } return { content: [ { type: "text", text: `Error: ${ error instanceof Error ? error.message : String(error) }`, }, ], }; } }); return server; } // Get Keycloak configuration from environment variables const config: KeycloakConfig = { baseUrl: process.env.KEYCLOAK_URL || "http://localhost:8080", adminUsername: process.env.KEYCLOAK_ADMIN || "admin", adminPassword: process.env.KEYCLOAK_ADMIN_PASSWORD || "admin", }; // Main function to start the server async function main(): Promise<void> { try { const transport = new StdioServerTransport(); const server = await createKeycloakMcpServer(config); await server.connect(transport); console.error("@octodet/keycloak-mcp server running on stdio"); // Handle termination signals process.on("SIGINT", async () => { await server.close(); process.exit(0); }); } catch (error) { console.error( "Server error:", error instanceof Error ? error.message : String(error) ); process.exit(1); } } main().catch((error) => { console.error("Unhandled error:", error); process.exit(1); });

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/Octodet/keycloak-mcp'

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