Skip to main content
Glama
idoyudha

mcp-keycloak

by idoyudha

create_user

Create a new user in a Keycloak realm with required username and optional email, name, enabled status, email verification, temporary password, and attributes.

Instructions

Create a new user.

Args:
    username: Username for the new user
    email: Email address
    first_name: First name
    last_name: Last name
    enabled: Whether the user is enabled
    email_verified: Whether the email is verified
    temporary_password: Initial password (user will be required to change it)
    attributes: Additional user attributes
    realm: Target realm (uses default if not specified)

Returns:
    Dict with status and location of created user

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
usernameYes
emailNo
first_nameNo
last_nameNo
enabledNo
email_verifiedNo
temporary_passwordNo
attributesNo
realmNo

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • The main handler function for the create_user tool. Decorated with @mcp.tool(), it accepts user parameters (username, email, first_name, last_name, enabled, email_verified, temporary_password, attributes, realm), builds a user_data dict, and makes a POST request to Keycloak's /users endpoint to create a new user. Returns a status dict.
    @mcp.tool()
    async def create_user(
        username: str,
        email: Optional[str] = None,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        enabled: bool = True,
        email_verified: bool = False,
        temporary_password: Optional[str] = None,
        attributes: Optional[Dict[str, List[str]]] = None,
        realm: Optional[str] = None,
    ) -> Dict[str, str]:
        """
        Create a new user.
    
        Args:
            username: Username for the new user
            email: Email address
            first_name: First name
            last_name: Last name
            enabled: Whether the user is enabled
            email_verified: Whether the email is verified
            temporary_password: Initial password (user will be required to change it)
            attributes: Additional user attributes
            realm: Target realm (uses default if not specified)
    
        Returns:
            Dict with status and location of created user
        """
        user_data = {
            "username": username,
            "enabled": enabled,
            "emailVerified": email_verified,
        }
    
        if email:
            user_data["email"] = email
        if first_name:
            user_data["firstName"] = first_name
        if last_name:
            user_data["lastName"] = last_name
        if attributes:
            user_data["attributes"] = attributes
    
        if temporary_password:
            user_data["credentials"] = [
                {"type": "password", "value": temporary_password, "temporary": True}
            ]
    
        # Create user returns no content, but includes Location header
        await client._make_request("POST", "/users", data=user_data, realm=realm)
        return {"status": "created", "message": f"User {username} created successfully"}
  • The FastMCP server instance ('mcp') is created here. The @mcp.tool() decorator on create_user registers it as an MCP tool.
    from mcp.server.fastmcp import FastMCP
    
    # Initialize FastMCP server
    mcp = FastMCP("Keycloak MCP Server", dependencies=["dotenv"])
  • src/main.py:17-23 (registration)
    Main entry point imports the tools modules (including user_tools) to ensure all @mcp.tool() decorated functions are registered with the server.
    # Import all tool modules to register them with the MCP server
    from . import tools  # noqa: F401
    from .tools import user_tools  # noqa: F401
    from .tools import client_tools  # noqa: F401
    from .tools import realm_tools  # noqa: F401
    from .tools import role_tools  # noqa: F401
    from .tools import group_tools  # noqa: F401
  • KeycloakClient class used by create_user to make authenticated HTTP requests to Keycloak's admin API. The _make_request method handles the POST to /users.
    class KeycloakClient:
        def __init__(self):
            self.server_url = KEYCLOAK_CFG["server_url"]
            self.username = KEYCLOAK_CFG["username"]
            self.password = KEYCLOAK_CFG["password"]
            self.realm_name = (
                KEYCLOAK_CFG["realm_name"] if KEYCLOAK_CFG["realm_name"] else DEFAULT_REALM
            )
            self.client_id = KEYCLOAK_CFG["client_id"]
            self.client_secret = KEYCLOAK_CFG["client_secret"]
            self.token = None
            self.refresh_token = None
            self._client = None
    
        async def _ensure_client(self):
            """Ensure httpx async client exists"""
            if self._client is None:
                self._client = httpx.AsyncClient(timeout=DEFAULT_REQUEST_TIMEOUT)
            return self._client
    
        async def _get_token(self) -> str:
            """Get access token using username and password"""
            # Try new URL structure first, then fall back to old one
            token_url = f"{self.server_url}/auth/realms/{self.realm_name}/protocol/openid-connect/token"
    
            data = {
                "grant_type": "password",
                "username": self.username,
                "password": self.password,
                "client_id": "admin-cli",  # Using admin-cli for admin operations
            }
    
            client = await self._ensure_client()
            response = await client.post(token_url, data=data)
            response.raise_for_status()
    
            token_data = response.json()
            self.token = token_data["access_token"]
            self.refresh_token = token_data.get("refresh_token")
    
            return self.token
    
        async def _get_headers(self) -> Dict[str, str]:
            """Get headers with authorization token"""
            if not self.token:
                await self._get_token()
    
            return {
                "Authorization": f"Bearer {self.token}",
                "Content-Type": "application/json",
            }
    
        async def _make_request(
            self,
            method: str,
            endpoint: str,
            data: Optional[Dict] = None,
            params: Optional[Dict] = None,
            skip_realm: bool = False,
            realm: Optional[str] = None,
        ) -> Any:
            """Make authenticated request to Keycloak API"""
            if skip_realm:
                url = f"{self.server_url}/auth/admin{endpoint}"
            else:
                # Use provided realm or fall back to configured realm
                target_realm = realm if realm is not None else self.realm_name
                url = f"{self.server_url}/auth/admin/realms/{target_realm}{endpoint}"
    
            try:
                client = await self._ensure_client()
                headers = await self._get_headers()
    
                response = await client.request(
                    method=method,
                    url=url,
                    headers=headers,
                    json=data,
                    params=params,
                )
    
                # If token expired, refresh and retry
                if response.status_code == 401:
                    await self._get_token()
                    headers = await self._get_headers()
                    response = await client.request(
                        method=method,
                        url=url,
                        headers=headers,
                        json=data,
                        params=params,
                    )
    
                response.raise_for_status()
    
                if response.content:
                    return response.json()
                return None
    
            except httpx.RequestError as e:
                raise Exception(f"Keycloak API request failed: {str(e)}")
    
        async def close(self):
            """Close the httpx client"""
            if self._client:
                await self._client.aclose()
                self._client = None
Behavior3/5

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

Describes that temporary_password requires user to change it, but lacks details on error handling, side effects, or required permissions. No annotations provided, so description carries full burden, but it is moderately informative.

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

Conciseness4/5

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

Well-structured with Args and Returns sections, front-loaded with purpose. Could be slightly more concise, but lists all parameters clearly.

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

Completeness4/5

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

Covers all parameters and return value; lacks error scenarios or behavioral edge cases. With output schema present, description is adequate for a creation tool.

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

Parameters4/5

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

Adds meaning beyond the empty schema descriptions with brief explanations for each parameter (e.g., 'Initial password (user will be required to change it)') and notes default behavior for 'realm'. Schema coverage is 0%, so description compensates well.

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?

The description clearly states 'Create a new user' with a specific verb and resource, distinguishing it from sibling tools like update_user and delete_user.

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

Usage Guidelines2/5

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

No guidance on when to use this tool vs alternatives; no mention of prerequisites or conditions (e.g., whether username must be unique, what happens if user exists).

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

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