Skip to main content
Glama
Octodet

Advanced Keycloak MCP server

by Octodet

update-user-roles

Add or remove client roles for a user in a Keycloak realm and client. Specify roles to add or remove, along with realm, user ID, and client ID.

Instructions

Add and/or remove client roles for a user in a specific realm and client

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
realmYesRealm name
userIdYesUser ID
clientIdYesClient ID
rolesToAddNoRoles to add
rolesToRemoveNoRoles to remove

Implementation Reference

  • The `updateUserRoles` method on the KeycloakService class that executes the tool logic: finds the client, fetches all client roles, adds specified roles via addClientRoleMappings, removes specified roles via delClientRoleMappings, and returns results including any errors for missing 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 };
    }
  • The case handler in the CallToolRequestSchema switch that parses args using UpdateUserRolesSchema, calls keycloakService.updateUserRoles(), and formats the response text.
    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(", ")}` : ""}`,
          },
        ],
  • Zod schema `UpdateUserRolesSchema` defining the input shape: realm, userId, clientId (required strings) and optional rolesToAdd/rolesToRemove (arrays of strings).
    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(),
    });
  • src/index.ts:409-423 (registration)
    Tool registration in the ListToolsRequestSchema handler, defining name 'update-user-roles', description, and inputSchema properties.
    {
      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"],
      },
    },
  • Helper logic within updateUserRoles: client lookup by ID first then by clientId fallback, and building a name-to-role map for role resolution.
    // 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
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, but description explains the core action (add/remove roles). However, it omits side effects, permissions, idempotency, or error behavior, which are important for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Single sentence, front-loaded with action verb, no redundancy. Efficient and to the point.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Despite simplicity, the description lacks completeness: no output schema, no explanation of response format, error cases, or prerequisites. For a modification tool, more context is needed for confident use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% with clear parameter descriptions; the tool description adds no extra semantic value beyond restating the action, so baseline 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states the tool adds/removes client roles for a user in a realm and client, distinguishing it from sibling tools that handle user CRUD, listing, or password reset.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by the description, but no explicit guidance on when to use vs. alternatives like list-roles or when to avoid; lacks when-not or prerequisite conditions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

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