Skip to main content
Glama
shigechika

keycloak-mcp

by shigechika

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
KEYCLOAK_URLYesBase URL of the Keycloak server, e.g. https://keycloak.example.com
KEYCLOAK_REALMNoRealm namemaster
KEYCLOAK_CLIENT_IDYesService Account client ID
KEYCLOAK_SITES_ININoPath to INI file for IP-to-site labeling
KEYCLOAK_CLIENT_SECRETYesClient secret
KEYCLOAK_DEFAULT_DATE_FROM_HOURSNoDefault look-back window for event tools when date_from is omitted24

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
health_checkA

Report server version and KeyCloak backend connectivity / authentication.

Call this at session start (or after a tool-call timeout) to confirm the MCP is up, see which version is running, and verify the KeyCloak Admin API is reachable and the service account can authenticate. Lightweight: it acquires an admin access token via the Client Credentials Grant (reusing the cached client) and does NOT enumerate users, events, or sessions.

Always returns the same keys: status (healthy / degraded / error), service, version, keycloak_url (configured base URL, empty if unset), realm (configured realm), keycloak_version (None — not exposed by a cheap call), and auth (ok / error / missing-env). On a degraded or error result, detail carries the reason.

This description is the only place those value sets are written down. The READMEs used to repeat them, which is three copies to keep in step and two that an LLM never reads — it is handed this text.

count_usersA

Get total user count in the realm.

search_usersA

Search users by username, email, first name, or last name.

Args: query: Search string (partial match). max_results: Maximum results to return (default 20).

get_userA

Get detailed user information by exact username (email).

If KEYCLOAK_USER_ATTRIBUTE_WHITELIST names any custom attribute keys, this also does one extra by-ID lookup and appends whichever of those keys are present on the user (the search endpoint used to resolve the username returns a brief representation that omits attributes entirely). A whitelisted key whose name looks credential-shaped (contains "password", "secret", "token", etc. — see _looks_like_credential_key) is reported as blocked rather than shown, as a safety net on top of the whitelist itself.

Args: username: Exact username (e.g., user@example.com).

reset_passwordA

Reset a user's password.

Args: username: Exact username (email). password: New password to set. temporary: If True, user must change password on next login.

reset_passwords_batchA

Reset passwords for multiple users from CSV text.

Each line should be: username,password If password column is empty, a random 12-char password is generated and included in the response (the caller cannot recover it otherwise). Caller-supplied passwords are never echoed back.

Args: csv_text: CSV text with username,password per line (header optional). temporary: If True, users must change password on next login.

get_user_sessionsC

Get active sessions for a user.

Args: username: Exact username (email).

logout_userA

Force logout a user by removing all their active sessions.

Args: username: Exact username (email).

set_user_enabledA

Enable or disable a user account.

Disabling blocks all authentication (SSO logins) for the user — the containment action for a compromised or decommissioned account. Only the enabled flag is changed; custom attributes are preserved.

Disabling does not terminate existing sessions (an already-issued token stays valid until it expires), so when disabling this reports how many sessions remain and to run logout_user to end them immediately.

Args: username: Exact username (email). enabled: True to enable, False to disable.

get_user_credentialsA

List the credential types configured for one user (password, otp, webauthn, …).

Use this to check a single user's MFA status: an otp credential means TOTP/HOTP is configured. Reads /users/{id}/credentials (read-only; does not create a session).

Args: username: Exact username (email).

get_totp_usersA

Report how many users have TOTP (OTP) configured across the realm.

Enumerates users and inspects each one's credentials for an otp entry. KeyCloak has no bulk credential endpoint, so this makes one credential request per user (N+1) — expect it to be slow on large realms; bound it with max_users (which also short-circuits the user enumeration). Users whose credential lookup fails are counted separately and skipped, so a single transient error does not abort the whole scan.

Args: enabled_only: Only scan enabled users (default True). list_users: Include the list of usernames with TOTP (default True). max_users: Cap the number of users scanned. 0 (default) falls back to KEYCLOAK_MAX_USERS (default 5000) rather than the whole realm. The N+1 credential loop is also bounded by KEYCLOAK_DEADLINE, so a large realm returns a disclosed sample. When capped, the percentage covers only the sample, not the realm.

get_brute_force_statusA

Check if a user is temporarily locked due to brute force detection.

Args: username: Exact username (email).

list_user_groupsA

List groups a user belongs to.

Args: username: Exact username (email).

list_users_by_groupA

List all users in a group.

Args: group_name: Group name (partial match). max_results: Maximum results (default 100).

get_eventsA

Get KeyCloak events with optional filters.

Args: event_type: Event type filter (e.g., LOGIN, LOGIN_ERROR, UPDATE_PASSWORD). username: Filter by exact username (email). Resolved to user ID internally. client_id: Filter by client ID (SP name). ip_address: Filter events by source IP (client-side filter). date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). max_results: Maximum results (default 50).

get_login_statsA

Get login success/failure statistics with full pagination.

Time-bounded: this call stops after KEYCLOAK_DEADLINE seconds (default 45) and returns what it has; the counts are then a lower bound. The result then starts with a "PARTIAL RESULT" warning. Call again with a narrower window instead of retrying the same call. A wide window on a busy day is what triggers it.

Args: date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). Empty for all.

get_login_stats_by_hourA

Get login statistics broken down by hour (local time).

Time-bounded: this call stops after KEYCLOAK_DEADLINE seconds (default 45) and returns what it has; the counts are then a lower bound. The result then starts with a "PARTIAL RESULT" warning. Call again with a narrower window instead of retrying the same call. A wide window on a busy day is what triggers it.

Args: date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). Empty for all.

get_login_failures_by_ipA

Get login failure statistics broken down by source IP.

Time-bounded: this call stops after KEYCLOAK_DEADLINE seconds (default 45) and returns what it has; the counts are then a lower bound. The result then starts with a "PARTIAL RESULT" warning. Call again with a narrower window instead of retrying the same call. A wide window on a busy day is what triggers it.

Args: date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). Empty for all. top: Number of top IPs to show (default 20).

get_ip_activityA

Exhaustive investigation of all activity from one source IP address.

Unlike get_events(ip_address=...), which filters a single page and can miss activity outside the most recent max_results events, this tool fully paginates every requested event type (via get_events_all) before filtering by IP, so the result is exhaustive over the requested date range. Use this for brute-force / credential-stuffing / shared-workstation investigations where get_login_failures_by_ip told you which IP to look at and you now need the full picture for that one IP.

Returns a fixed-shape dict (JSON), not formatted text — every key below is always present, even when zero events match.

Returns: error: None on success. Set to a descriptive message if event_types resolved to no event types (e.g. empty or all-whitespace/commas); every other key is still present, with an empty/zero result in that case (no data was fetched). ip_address: Echoes the input. site: Site name from KEYCLOAK_SITES_INI, or null if unmatched or unconfigured (see sites_configured to tell those apart). sites_configured: True if KEYCLOAK_SITES_INI was loaded at all. date_from / date_to: The resolved date range actually scanned. event_types: The event types scanned (echoes the input, split). summary: total_events, login_success, login_failure, unique_users, unique_clients, first_seen/last_seen (ISO 8601, null if no match). login_success/login_failure classify EVERY scanned event type by whether its type ends in "_ERROR" (matching the users/clients breakdown below), not just literal LOGIN/LOGIN_ERROR — so widening event_types always keeps these numbers reconciled with the per-user/per-client totals. Always computed over the FULL matched set, unaffected by max_timeline truncation. users: Per-user breakdown (success/failure counts, distinct error codes), sorted by total activity descending. Note: successful LOGIN events often carry only a userId (UUID) while LOGIN_ERROR carries details.username — this tool keys on username-or-userId-or-"unknown", so the same human can legitimately appear under two different keys across success vs. failure events. clients: Per-client (SP) breakdown, same shape, sorted descending. timeline: Chronological event list, capped at max_timeline (most recent kept on overflow — see truncated). max_timeline<=0 returns an empty timeline. truncated: True if timeline was capped; summary/users/clients are never affected by this cap. events_capped: True if event pagination itself was cut short by the wall-clock deadline (KEYCLOAK_DEADLINE) or the per-type cap (KEYCLOAK_MAX_EVENTS) — i.e. the window was too wide and the WHOLE result (summary/users/clients/timeline) is incomplete. Distinct from truncated, which only trims the timeline of an otherwise-complete scan. Narrow date_from when this is true.

Time-bounded: this call stops after KEYCLOAK_DEADLINE seconds (default 45) and returns what it has; the counts are then a lower bound. The returned dict then has events_capped: true (no warning text); narrow date_from / date_to. Call again with a narrower window instead of retrying the same call. A wide window on a busy day is what triggers it.

Args: ip_address: Source IP to investigate. Compared against KeyCloak's recorded ipAddress field after normalizing both sides through Python's ipaddress module (so equivalent IPv6 notations like "::1" and "0:0:0:0:0:0:0:1" match); falls back to a raw string compare if either side doesn't parse as an IP. event_types: Comma-separated KeyCloak event types to scan (default "LOGIN,LOGIN_ERROR"). Widen with e.g. "LOGIN,LOGIN_ERROR,LOGOUT,UPDATE_PASSWORD,CLIENT_LOGIN,CLIENT_LOGIN_ERROR" for a broader sweep. Must resolve to at least one type. date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). Widening the window means fully paginating every event type over that window before filtering — expect it to be slower on large realms. date_to: End date (YYYY-MM-DD). Empty for open-ended. max_timeline: Cap on the number of most-recent timeline entries returned (default 200; <=0 means no timeline entries). Does not affect summary/users/clients.

spray_checkA

Detect password-spray sources and name the accounts they breached — one rule, one call.

For every EXTERNAL source IP (anything outside the ranges declared in KEYCLOAK_SITES_INI) seen in LOGIN / LOGIN_ERROR events during the last hours, compute distinct users and success rate. An IP is a spray source when unique_users >= min_users AND success_rate < max_success_rate. Its successful logins are the breach CANDIDATES; whether they may be called breached depends on the row's confidence.

The breach list is built ONLY from LOGIN events whose source IP is the flagged IP, inside the window. Every entry carries the evidence tuple {time, ip, username, user_id, client_id}. A compromised account can therefore never be reported without an actual login event from the spray source — do not add names that are not in spray[].breached.

confidence separates a spray from a shared egress (school NAT, home line, VDI) that merely looks like one by volume. It is "low" — treat the successes as "verify with the owner", never publish them as breached — when any of these signals holds: user_success_rate (distinct users that logged in at least once ÷ distinct users) >= max_user_success_rate (real sprays sit at 0.0–0.06; a school NAT with students retyping a mistyped domain sat at 0.47), or failure_concentration (share of failures on the single most-failing username) >= max_failure_concentration (one locked-out user retrying from a shared line produced 0.91), or the IP is in KEYCLOAK_KNOWN_EGRESS. Only confidence: high rows are a breach verdict. Read top_failed_users and not_found_domains (domains of usernames that do not exist — typos of the real domain are humans, not a scraped list) before writing anything up.

Returns a fixed-shape dict: window: {hours, since, until} actually scanned. complete: False if event pagination was cut short (KEYCLOAK_DEADLINE / KEYCLOAK_MAX_EVENTS). When False, treat the result as a lower bound and do NOT publish a definitive verdict; narrow hours and retry. spray: flagged IPs (see external_ips for the row shape), each with breached = list of evidence tuples; confidence: high rows first. external_ips: every external IP with at least min_report_users distinct users, flagged or not, sorted flagged-first, then high-confidence first, then by ascending success rate — so near-misses (e.g. 8 users at 14%) are visible without a second rule. Row: ip, known_egress, flagged, confidence ("high"/"low"), signals (list of the reasons for "low"), unique_users, users_with_success, user_success_rate, failure_concentration, top_failed_users (up to 5 {username, failures}), not_found_domains (domain -> count for user_not_found usernames), attempts, successes, failures, success_rate, errors (error-code counter; user_not_found mixed with invalid_user_credentials on many DIFFERENT names indicates a scraped username list — on the same few names it is a human retyping), first_seen, last_seen, unresolved_user_ids, breached. breached_total: number of evidence tuples across all flagged IPs. breached_low_confidence: how many of those sit on confidence: low rows (candidates to verify, not breaches). internal_events_excluded: events dropped because the IP is internal. resolves_used / resolve_capped: how many GET /users/{id} lookups were spent resolving success userIds, and whether max_resolves (or the shared deadline) stopped further lookups. A row's unresolved_user_ids counts successes keyed by bare userId; if that is non-zero on a flagged row, unique_users may be slightly over-counted and breached[].username is the id. known_egress_configured: whether KEYCLOAK_KNOWN_EGRESS is set. IPs in those ranges are LABELED known_egress: true, never excluded — a shared VDI/VPN/proxy egress with many real users is expected to show a high success rate and usually is not flagged anyway.

Users are keyed by lowercased username on both sides: LOGIN_ERROR carries details.username; LOGIN usually carries only userId. userIds are mapped from the fetched events first (any event carrying both fields), then via GET /users/{id} — only for IPs below the success-rate ceiling (the only ones that can be flagged), at most max_resolves times, and never past the shared KEYCLOAK_DEADLINE.

Time-bounded: this call stops after KEYCLOAK_DEADLINE seconds (default 45) and returns what it has; the counts are then a lower bound. The returned dict then has complete: false (no warning text); use a smaller hours. Call again with a narrower window instead of retrying the same call. A wide window on a busy day is what triggers it.

Args: hours: Look-back window (default 24 — sized for a once-a-day patrol). min_users: Distinct users an IP must touch to count as a spray (default 10). max_success_rate: Success-rate ceiling for a spray (default 0.2). min_report_users: Distinct users an IP needs to appear in external_ips at all (default 3; clamped to min_users). max_resolves: Cap on GET /users/{id} lookups per call (default 200). max_user_success_rate: user_success_rate at or above this marks the row confidence: low (default 0.3). max_failure_concentration: failure_concentration at or above this marks the row confidence: low (default 0.5).

get_login_stats_by_clientA

Get login statistics broken down by client (SP).

Time-bounded: this call stops after KEYCLOAK_DEADLINE seconds (default 45) and returns what it has; the counts are then a lower bound. The result then starts with a "PARTIAL RESULT" warning. Call again with a narrower window instead of retrying the same call. A wide window on a busy day is what triggers it.

Args: date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). Empty for all.

detect_login_loopsA

Detect users with rapid repeated logins (possible redirect loops).

Scans all LOGIN events and finds users who logged in more than threshold times within window_seconds.

Time-bounded: this call stops after KEYCLOAK_DEADLINE seconds (default 45) and returns what it has; the counts are then a lower bound. The result then starts with a "PARTIAL RESULT" warning. Call again with a narrower window instead of retrying the same call. A wide window on a busy day is what triggers it.

Args: date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). Empty for all. threshold: Minimum logins within the window to flag (default 10). window_seconds: Time window in seconds (default 60). top: Number of top users to show (default 20). Use 0 for all.

get_password_update_eventsA

Get password update events.

Args: date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). max_results: Maximum results (default 100).

get_admin_eventsA

Get KeyCloak admin events (changes performed via the Admin REST API).

Admin events record operations performed by service accounts or admin users — e.g. custom user attribute updates (provisioning_flag), role / group assignments, client configuration changes. These are distinct from user events (login / password change). Use this when UPDATE_PROFILE in get_events is empty but an attribute is known to have changed.

Args: operation_types: Comma-separated list of CREATE, UPDATE, DELETE, ACTION. resource_types: Comma-separated list of USER, CLIENT, ROLE, GROUP, REALM_ROLE, etc. resource_path: Filter by resource path (e.g. "users/{userId}"). date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). max_results: Maximum results (default 50). max_repr: Max chars of the representation field. 0 = omit, -1 = full.

get_user_attribute_historyA

Get admin-side attribute change history for a single user.

Queries admin events scoped to users/{userId} with UPDATE / ACTION operations. Intended for tracking custom attribute changes such as provisioning_flag which are written by admin API and do not surface in get_events (which only shows user-driven events like LOGIN / UPDATE_PASSWORD).

Args: username: Exact username (email). date_from: Start date (YYYY-MM-DD). Defaults to last 24h when omitted (KEYCLOAK_DEFAULT_DATE_FROM_HOURS). date_to: End date (YYYY-MM-DD). max_results: Maximum results (default 100). max_repr: Max chars of the representation field. 0 = omit, -1 = full.

get_session_statsA

Get active session count per client.

get_client_sessionsA

Get active sessions for a specific client (SP).

Args: client_id: Client ID (e.g., 'xflow', 'shadowserver'). max_results: Maximum results (default 100).

list_clientsA

List all SAML/OIDC clients in the realm.

get_clientA

Show one client's configuration, including its authentication flow overrides.

Reports an explicit allowlist of fields rather than the raw client representation. A client representation can carry secret, registrationAccessToken and, for SAML clients, signing material under attributes; attributes and protocolMappers are therefore omitted entirely rather than filtered, so nothing credential-shaped reaches tool output, hence LLM context.

The headline is authenticationFlowBindingOverrides: pinning one client to a non-default browser flow is how a single SP is made to require OTP while the realm default stays untouched. KeyCloak stores those overrides as flow IDs, so they are resolved to flow aliases here.

Args: client_id: The clientId (not the internal UUID).

get_realm_rolesA

List all realm-level roles.

get_realm_security_defensesA

Show the realm's security-defense settings (read-only).

Reports the realm-level security configuration that the admin console groups under "Security defenses":

  • Brute force detection: whether it is enabled, the lockout strategy, and the thresholds (max login failures, wait increments, reset window).

  • Password policy.

  • Browser security headers.

Use this to verify that brute-force protection is actually turned on and how aggressively it locks accounts — the per-user get_brute_force_status only reflects runtime state, not whether the policy itself is configured.

daily_briefA

Run a morning Keycloak health check.

Checks (all scoped to the last since_hours hours):

  • Login statistics (success / failure totals, top failing IPs)

  • Active sessions by client

  • Password update events

  • Admin events (CREATE/UPDATE/DELETE on USER/CLIENT resources)

A single IP with login failures >= ip_failure_threshold is flagged as WARNING (possible brute-force). Independently, the same login events are run through the spray_check rule (external IP, >= 10 distinct users, success rate < 20%); a match is a [SPRAY] WARNING and the "Spray check" section lists the breached accounts with their evidence tuples (time / ip / username / client). Only accounts in that list may be called breached — see spray_check for the full row shape and to widen the window or tune the thresholds.

since_hours defaults to 18 (≈ previous 15:00 for a 09:00 morning run).

Output tiers:

  • CRITICAL — API connection failure

  • WARNING — anomalies detected

  • OK — clean

Args: since_hours: Look-back window in hours (default 18). ip_failure_threshold: Login failures from a single IP that triggers a WARNING (default 50).

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A3.6/5.0

Scored across 32 tools

Disambiguation4/5

Most tools are clearly distinct by resource and dimension (e.g., login stats by client/hour vs. IP, user vs. client vs. session stats). Some overlap exists in event retrieval (get_events, get_admin_events, get_password_update_events) and session inspection, but the detailed descriptions help an agent differentiate them.

Naming Consistency4/5

Names predominantly follow a consistent snake_case verb_noun pattern (get_*, list_*, reset_*, set_*, search_*). A few report-style tools (daily_brief, spray_check, health_check) deviate from strict verb_noun, but casing and readability remain consistent.

Tool Count2/5

32 tools is excessive for a single MCP server; many event and statistics tools (e.g., get_login_stats, get_login_stats_by_client, get_login_stats_by_hour, get_login_failures_by_ip) could be consolidated via parameters. This volume increases navigation overhead and cognitive load for an agent.

Completeness3/5

The server covers security monitoring and containment well (events, stats, disable user, logout, password reset) but lacks core administrative CRUD operations like create/delete user, group membership management, client/role creation, and clearing brute-force lockouts. These are notable gaps for a Keycloak admin surface.

Maintenance

ActivityActive
ResponsivenessResponsive