{
  "markdown": "<!-- mcp-name: io.github.shigechika/keycloak-mcp -->\n\n# keycloak-mcp\n\nEnglish | [日本語](README.ja.md)\n\nAn MCP (Model Context Protocol) server for the [KeyCloak](https://www.keycloak.org/) Admin REST API.\n\nDocumentation: <https://shigechika.github.io/keycloak-mcp/>\n\nAuthenticates via a Service Account (**Client Credentials Grant**), so no human password or TOTP is involved. Also Infinispan-safe — it never creates user sessions and never hits the userinfo endpoint.\n\n## Features\n\n### Users\n\n| Tool | Description |\n|------|-------------|\n| `count_users` | Total user count in the realm |\n| `search_users` | Partial-match search (username / email / name) |\n| `get_user` | Full detail for an exact username |\n| `reset_password` | Reset one user's password |\n| `reset_passwords_batch` | Bulk reset from CSV (`username,password` per line; blank password is generated) |\n| `get_user_sessions` | Active sessions for one user, timestamps in local time |\n| `logout_user` | Kill all active sessions for one user |\n| `set_user_enabled` | Enable or disable one user; disabling blocks all logins (custom attributes preserved) |\n\n### MFA / Credentials\n\n| Tool | Description |\n|------|-------------|\n| `get_user_credentials` | Credential types configured for one user; an `otp` entry means TOTP/HOTP is set up |\n| `get_totp_users` | Realm-wide TOTP adoption: how many users have an `otp` credential, with percentage and (optionally) the user list. Enumerates users and reads each one's credentials (N+1; bound with `max_users`) |\n\n### Groups\n\n| Tool | Description |\n|------|-------------|\n| `list_user_groups` | Which groups a user belongs to |\n| `list_users_by_group` | Members of a group |\n\n### Security\n\n| Tool | Description |\n|------|-------------|\n| `get_brute_force_status` | Whether a user is currently locked by brute-force detection |\n| `get_realm_security_defenses` | Realm-level security policy: whether brute-force detection is enabled and its thresholds, the password policy, and browser security headers |\n| `get_login_failures_by_ip` | Failure breakdown by source IP (site-labeled when `KEYCLOAK_SITES_INI` is set) |\n| `get_ip_activity` | Exhaustive investigation of one source IP: success/failure counts, affected users/clients, timeline. Returns structured JSON. |\n| `detect_login_loops` | Flag users who logged in too many times in a short window (redirect loops) |\n\n### Events\n\n| Tool | Description |\n|------|-------------|\n| `get_events` | Filter by type, username, client, IP, and date range. Username is resolved to user ID internally. Failure events include KeyCloak's `error` field (e.g. `invalid_user_credentials`). |\n| `get_login_stats` | Login success/failure totals, paginated across all results |\n| `get_login_stats_by_hour` | Logins bucketed by hour of day (local time) |\n| `get_login_stats_by_client` | Logins bucketed by client / SP |\n| `get_password_update_events` | `UPDATE_PASSWORD` history |\n\n### Admin Events\n\n`get_events` only sees *user* events. Actions driven by an admin — or by a service account writing custom attributes — don't show up there. The admin-event endpoint fills that gap.\n\n| Tool | Description |\n|------|-------------|\n| `get_admin_events` | Filter by operation (CREATE / UPDATE / DELETE / ACTION), resource type (USER / CLIENT / ROLE / GROUP / …), resource path, and date range |\n| `get_user_attribute_history` | UPDATE/ACTION events scoped to one user — handy for tracking when a custom attribute (e.g. `provisioning_flag`) was written by an automated pipeline |\n\nBoth tools accept `max_repr` to control the representation payload: positive = truncate to N chars (default 500), `0` = omit, negative = include in full.\n\n### Sessions & Clients\n\n| Tool | Description |\n|------|-------------|\n| `get_session_stats` | Active session count per client |\n| `get_client_sessions` | Active sessions for one client (SP) |\n| `list_clients` | SAML and OIDC clients in the realm |\n| `get_realm_roles` | Realm-level roles |\n\n### Morning Patrol\n\n| Tool | Description |\n|------|-------------|\n| `health_check` | Report the running server version and verify the KeyCloak backend is reachable and the service account can authenticate. Lightweight (one token request; no user/event/session scans). Returns a fixed-shape dict with `status`, `auth`, and the configured URL/realm. The tool's own description enumerates the values those fields can take, and is the one place that does. |\n| `daily_brief` | One-shot morning health check: login stats, brute-force IPs, active sessions, password updates, and admin events in a single Markdown summary. IPs exceeding `ip_failure_threshold` failures (default 50) are flagged **WARNING**; API errors surface as **CRITICAL**. `since_hours` controls the look-back window (default 18 h). |\n\n## Setup\n\n```bash\n# uv\nuv pip install keycloak-mcp\n\n# pip\npip install keycloak-mcp\n```\n\nFrom source:\n\n```bash\ngit clone https://github.com/shigechika/keycloak-mcp.git\ncd keycloak-mcp\n\n# uv\nuv sync\n\n# pip\npip install -e .\n```\n\n## Configuration\n\n| Variable | Description | Default |\n|---|---|---|\n| `KEYCLOAK_URL` | Base URL, e.g. `https://keycloak.example.com` | *required* |\n| `KEYCLOAK_REALM` | Realm name | `master` |\n| `KEYCLOAK_CLIENT_ID` | Service Account client ID | *required* |\n| `KEYCLOAK_CLIENT_SECRET` | Client secret | *required* |\n| `KEYCLOAK_SITES_INI` | INI file for IP-to-site labeling (see below) | *unset* |\n| `KEYCLOAK_DEFAULT_DATE_FROM_HOURS` | Default look-back window for event tools when `date_from` is omitted. Set to `0` to scan full history (can hang on large realms). | `24` |\n| `KEYCLOAK_DEADLINE` | Per-call wall-clock budget (seconds) for the heavy event/TOTP tools. When a wide window / large realm would exceed it, the tool stops and returns a **disclosed partial** (⚠️ warning) instead of running past the client's ~60s gateway timeout and hammering KeyCloak. `0` or negative disables. | `45` |\n| `KEYCLOAK_MAX_EVENTS` | Per-pagination cap on events fetched by the event tools (also bounds how deep the slow high-offset pagination goes). Over the cap the result is a disclosed partial. `0` or negative disables. | `200000` |\n| `KEYCLOAK_MAX_USERS` | Default cap on users scanned by `get_totp_users` when its `max_users` argument is `0` (each user costs one credential call). `0` or negative disables (whole realm, bounded only by `KEYCLOAK_DEADLINE`). | `5000` |\n| `KEYCLOAK_USER_ATTRIBUTE_WHITELIST` | Comma-separated custom user-attribute keys that `get_user` is allowed to surface. Unset by default: `get_user` only ever returns username/name/email/enabled/created, since the search endpoint it resolves the username through returns a brief representation with no `attributes` at all. Opting a key in makes `get_user` do one extra by-ID lookup and append that attribute's value when present. Everything else stays out of tool output. As a safety net, a whitelisted key whose name looks credential-shaped (contains `password`, `secret`, `token`, etc.) is reported as blocked rather than shown — not a guarantee, since a credential attribute named outside that pattern list still gets through. | *unset* |\n\n### KeyCloak client setup\n\n1. Create a new client in the KeyCloak admin console.\n2. Turn on **Client authentication** and **Service account roles**.\n3. Give it `view-users`, `view-events`, `view-clients`, and — only if you need password reset — `manage-users`.\n\n### Write operations\n\nFour tools change state. Everything else only reads.\n\n| Tool | Admin API call |\n|---|---|\n| `reset_password` | `PUT /users/{id}/reset-password` |\n| `reset_passwords_batch` | the same call, once per CSV row |\n| `set_user_enabled` | `PUT /users/{id}` with `enabled` toggled |\n| `logout_user` | `POST /users/{id}/logout` |\n\nAll four need `manage-users` on the Service Account. **Leave that role off and the\nserver is read-only**: those four tools fail with `403` and every other tool keeps\nworking, so a realm can be handed to Claude for investigation without granting any\nability to modify it. Grant `manage-users` only when account recovery or\ncontainment is part of the job.\n\n### Verify your setup\n\nAfter setting the environment variables, run `--check` to confirm authentication works before wiring it into an MCP client:\n\n```bash\nexport KEYCLOAK_URL=https://keycloak.example.com\nexport KEYCLOAK_REALM=my-realm\nexport KEYCLOAK_CLIENT_ID=keycloak-mcp\nexport KEYCLOAK_CLIENT_SECRET=your-secret\nkeycloak-mcp --check\n# HTTP Request: POST https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token \"HTTP/1.1 200 OK\"\n# OK: authenticated to https://keycloak.example.com/admin/realms/my-realm\n```\n\nExit codes: `0` success, `1` configuration error (missing variable), `2` authentication error.\n\n### IP-to-site labeling (optional)\n\nPoint `KEYCLOAK_SITES_INI` at an INI file if you want IP addresses in tool output to be tagged with your site names. Tools like `get_user_sessions`, `get_events`, and `get_login_failures_by_ip` pick it up automatically; anything outside your declared ranges is labeled `external`. Leave the variable unset and IPs are shown as-is.\n\nSee [`sites.ini.example`](sites.ini.example). A minimal file:\n\n```ini\n[hq]\nname = HQ (Tokyo)\nipv4 = 192.0.2.0/24, 198.51.100.0/24\nipv6 = 2001:db8:1::/48\n\n[vpn]\nname = VPN\nipv4 = 10.0.0.0/8, 172.16.0.0/12\n```\n\nOne site per `[section]`. `name` is the display label (falls back to the section name). `ipv4` / `ipv6` take comma-separated CIDRs; a single host is `/32` or `/128`. Matching is first-match in file order — put specific ranges before broad ones.\n\n## Usage\n\n### Claude Code (plugin)\n\nThis repository doubles as a single-plugin marketplace, so Claude Code can install\nthe server for you:\n\n```\n/plugin marketplace add shigechika/keycloak-mcp\n/plugin install keycloak-mcp@keycloak-mcp\n```\n\nThe plugin launches `uvx keycloak-mcp` and reads the same environment variables\ndescribed in [Configuration](#configuration); export them before starting Claude\nCode. `KEYCLOAK_REALM` falls back to `master` and `KEYCLOAK_SITES_INI` may stay\nunset.\n\n`uvx` must be on the `PATH` of the process that runs Claude Code — a login\nshell usually has it, but a GUI-launched app may not; install\n[uv](https://docs.astral.sh/uv/) system-wide if the plugin fails to start.\n\n### Claude Code (manual)\n\nIn `.mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"keycloak-mcp\": {\n      \"type\": \"stdio\",\n      \"command\": \"keycloak-mcp\",\n      \"env\": {\n        \"KEYCLOAK_URL\": \"https://keycloak.example.com\",\n        \"KEYCLOAK_REALM\": \"my-realm\",\n        \"KEYCLOAK_CLIENT_ID\": \"keycloak-mcp\",\n        \"KEYCLOAK_CLIENT_SECRET\": \"\"\n      }\n    }\n  }\n}\n```\n\n### Claude Desktop\n\nIn `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"keycloak-mcp\": {\n      \"command\": \"keycloak-mcp\",\n      \"env\": {\n        \"KEYCLOAK_URL\": \"https://keycloak.example.com\",\n        \"KEYCLOAK_REALM\": \"my-realm\",\n        \"KEYCLOAK_CLIENT_ID\": \"keycloak-mcp\",\n        \"KEYCLOAK_CLIENT_SECRET\": \"\"\n      }\n    }\n  }\n}\n```\n\n### From a shell\n\n```bash\nexport KEYCLOAK_URL=https://keycloak.example.com\nexport KEYCLOAK_REALM=my-realm\nexport KEYCLOAK_CLIENT_ID=keycloak-mcp\nexport KEYCLOAK_CLIENT_SECRET=your-secret\nkeycloak-mcp\n```\n\n### CLI\n\n```bash\nkeycloak-mcp --version   # Print version and exit\nkeycloak-mcp --help      # Show usage and required environment variables\nkeycloak-mcp --check     # Verify env vars and authentication, then exit\nkeycloak-mcp             # Run the MCP STDIO server (default)\n```\n\nNo-argument mode is the normal one — that's how MCP clients launch it.\n\n## Development\n\n```bash\ngit clone https://github.com/shigechika/keycloak-mcp.git\ncd keycloak-mcp\n\n# uv\nuv sync --dev\nuv run pytest -v\nuv run ruff check .\n\n# pip\npython3 -m venv .venv\n.venv/bin/pip install -e . && .venv/bin/pip install pytest pytest-cov respx ruff\n.venv/bin/pytest -v\n.venv/bin/ruff check .\n```\n\n### Live smoke test\n\n`pytest` checks logic against fixtures; it cannot tell you that a tool has\nstopped returning real data. `scripts/smoke_test.py` runs **every registered\ntool** against the configured realm and fails on empty, malformed or error\nanswers:\n\n```bash\n# needs KEYCLOAK_URL / KEYCLOAK_CLIENT_ID / KEYCLOAK_CLIENT_SECRET\nuv run python scripts/smoke_test.py\nuv run python scripts/smoke_test.py --only login_stats --traceback\n```\n\n- **Read-only.** Every state-changing tool (`reset_password`, `logout_user`,\n  `set_user_enabled`, `reset_passwords_batch`) is skipped by name, and a test\n  enforces that. The report prints tool names and statuses only — never\n  payloads, and server-authored error text is redacted too (KeyCloak quotes the\n  username it was asked about); `--traceback` still shows the full text on the\n  operator's own terminal.\n- Arguments that would identify real users, groups or IPs are **discovered at\n  run time**, never written into `scripts/smoke_probes.py`.\n- CI enforces the cheap half: a tool registered without a probe spec fails the\n  build (`tests/test_smoke_probes.py`), so adding a tool forces the question\n  \"how would we know it works?\".\n- `scripts/smoke_harness.py` is the engine and holds no KeyCloak knowledge:\n  it is kept identical across the servers that share it, so fix engine bugs\n  once and sync the file rather than patching this copy.\n\n## License\n\nMIT\n",
  "bytes": 13199,
  "sha": "6b90a9fc84a87f75cc883afe531329e87cc6a3332bd262f4b9420591db87cb1c",
  "repo_slug": "shigechika/keycloak-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_shigechika_keycloak_mcp_8f5c34b3/readme"
}