skycloak-mcp
Model Context Protocol server for Skycloak managed Keycloak. Manage clusters, realms, applications, SSO and users from any MCP client.
Documentation
skycloak-mcp
Official Model Context Protocol server for Skycloak (managed Keycloak): manage your clusters, realms, applications, and SSO from any MCP client (Claude Desktop, Claude Code, Cursor).
> Status: early release. Tool coverage is growing; see the changelog for what's available.
Quick start
claude mcp add --transport http skycloak https://mcp.skycloak.ioNo API key, no client ID, no configuration. Your browser opens, you sign in to Skycloak, and the tools appear. Any MCP
client that speaks streamable HTTP works the same way: give it the URL and nothing else.
Then ask for something:
- "Which of my Keycloak clusters are behind on upgrades?"
- "Create a staging realm on the EU cluster with Google and GitHub sign-in."
- "Who was added to the production realm in the last week?"
- "Set up a SIEM destination that forwards admin events to our Datadog webhook."
Authentication & safety
- Hosted HTTP, with OAuth (no credential to configure). Point your client at `https://mcp.skycloak.io` with no header. The server answers `401` with a pointer to its RFC 9728 metadata at `/.well-known/oauth-protected-resource`, the client runs the browser authorization-code flow against the Skycloak login realm, and the access token it gets back is exchanged for a short-lived, workspace-scoped API key that the session runs on. The key lasts an hour and is renewed automatically. Nothing is stored in your client configuration.
- Hosted HTTP, with an API key. Create a key in the Skycloak dashboard and send it as `Authorization: Bearer ` (or `API-Key: `). Every request carries its own credential and acts only as that credential's workspace. The server keeps no session state, so a request never inherits another caller's. Keys are not verified before use: the Skycloak API is the authority, so an invalid key surfaces as a `401` on the first tool call rather than at connect time.
- Tools match your role. Over OAuth, the tool list is trimmed to what the session's scopes allow, so a read-only workspace member is not shown write tools that would answer `403`. With an API key the whole surface is registered, because a key's scopes are not visible to the server, and an unauthorized call surfaces as a `403` from the API.
- Local stdio. Run `skycloak-mcp init` and approve in your browser (OAuth 2.0 device authorization flow). It mints a workspace-scoped API key, stores it in your operating-system keychain, and detects your default workspace automatically (pass `--workspace ` to pick another). `skycloak-mcp logout` removes the stored key.
- Headless / CI. Set the `SKYCLOAK_API_KEY` environment variable (create a key in the Skycloak dashboard) to skip the browser entirely. It always takes precedence over the keychain.
- Writes are gated by your credential, not by a flag. The hosted server at `https://mcp.skycloak.io` runs write-capable, and what you can actually change is bounded by your key's scopes and your workspace role: a read-only member cannot mutate anything, whatever the tool list says. Add `?readonly=true` to the URL to force a read-only tool surface for a session. The local binary is the opposite way round and registers no write tools unless started with `--allow-writes`.
- Cluster credentials are opt-in. `get_cluster_credentials` returns a cluster's Keycloak admin credentials, which an assistant holding the key would then see, so `init` does not request that scope by default. Use a key that carries it: create one in the dashboard, or over stdio sign in with `skycloak-mcp init --allow-credentials`. Without it the tool returns a 403 that explains both routes.
- Destructive tools require confirmation: deleting a realm, for example, needs an explicit `confirm=true` argument.
- Requests are rate limited according to your Skycloak plan; on a `429` response the server surfaces `Retry-After`.
Tools
129 tools: 58 read-only and 71 write. Read-only tools are always available. On the hosted server the write tools are registered too and gated by your credential's scopes; the local binary registers them only when started with `--allow-writes`.
Tool names carry a `skycloak_` prefix that the table below omits, so `list_clusters` is `skycloak_list_clusters` in your client.
| Area | Read-only | Write (`--allow-writes`) |
|---|---|---|
| Clusters | `list_clusters`, `get_cluster`, `list_cluster_locations`, `list_cluster_types`, `list_cluster_features`, `list_cluster_versions`, `list_cluster_upgrades`, `get_cluster_upgrade_path`, `get_cluster_credentials`, `get_cluster_insights`, `get_cluster_maintenance_window` | `create_cluster`, `update_cluster`, `delete_cluster`, `cancel_cluster_upgrade`, `set_cluster_maintenance_window`, `delete_cluster_maintenance_window` |
| Edge security | `get_cluster_security`, `list_cluster_captcha_domains` | `update_cluster_security`, `add_cluster_captcha_domain`, `remove_cluster_captcha_domain` |
| Realms | `list_realms`, `get_realm` | `create_realm`, `update_realm`, `delete_realm` |
| Applications | `list_applications`, `get_application`, `list_application_roles`, `list_application_sessions` | `create_application`, `update_application`, `delete_application`, `assign_application_role`, `remove_application_role`, `rotate_application_secret` |
| Identity providers | `list_identity_providers`, `get_identity_provider`, `list_identity_provider_templates`, `discover_oidc` | `create_identity_provider` (OIDC), `update_identity_provider`, `delete_identity_provider`, `test_identity_provider` |
| Users, roles & groups | `list_realm_users`, `get_realm_user`, `list_realm_roles`, `get_realm_role`, `list_realm_groups`, `get_realm_group`, `list_realm_group_members`, `list_user_roles`, `list_user_groups` | `create_realm_user`, `update_realm_user`, `delete_realm_user`, `create_realm_role`, `update_realm_role`, `delete_realm_role`, `create_realm_group`, `update_realm_group`, `delete_realm_group`, `assign_realm_user_role`, `remove_realm_user_role`, `add_realm_user_to_group`, `remove_realm_user_from_group` |
| Custom domains | `list_domains`, `get_domain`, `list_domain_routes`, `get_domain_route` | `create_domain`, `verify_domain`, `delete_domain`, `create_domain_route`, `update_domain_route`, `delete_domain_route` |
| Branding & themes | `list_themes`, `get_theme`, `get_theme_assignment`, `get_client_theme_assignment`, `get_login_branding`, `get_email_branding`, `download_theme_content` | `set_theme_assignment`, `set_client_theme_assignment`, `update_theme`, `delete_theme`, `upsert_login_branding`, `delete_login_branding`, `upsert_email_branding`, `delete_email_branding` |
| Extensions | `list_extensions`, `list_cluster_extensions` | `install_extension`, `upgrade_extension`, `update_extension`, `uninstall_extension`, `delete_extension` |
| SMTP | `get_smtp` | `upsert_smtp`, `delete_smtp`, `test_smtp` |
| Exports & logs | `list_exports`, `get_export`, `get_logs`, `get_security_logs`, `query_events` | `create_export`, `delete_export`, `export_cluster_events` |
| Realm import & export | `get_realm_export`, `get_realm_import` | `create_realm_export`, `create_realm_import`, `create_realm_import_upload_url` |
| SIEM | `list_siem_destinations`, `get_siem_destination` | `create_siem_destination`, `update_siem_destination`, `delete_siem_destination`, `test_siem_destination` |
| Webhooks | `list_webhook_event_types`, `list_webhook_subscriptions`, `get_webhook_subscription` | `create_webhook_subscription`, `update_webhook_subscription`, `delete_webhook_subscription`, `test_webhook_subscription` |
Conventions: destructive tools (`delete_*`, `uninstall_extension`, `cancel_cluster_upgrade`) require `confirm=true`. `create_cluster` is asynchronous: poll `get_cluster` until the cluster is `available`. `create_domain` returns the DNS records the customer must create; `verify_domain` triggers a DNS check. `set_theme_assignment` activates a custom theme per Keycloak theme type (empty string resets to the built-in default). `update_cluster_security` leaves CAPTCHA settings untouched. Realm import/export moves one realm's configuration and is separate from `create_export`, which dumps a whole cluster's database: both are asynchronous, and the realm archive is always encrypted, so the password used to export it is needed to import it again. A realm can be imported straight from an existing export (`source_export_id`) or from an uploaded archive (`create_realm_import_upload_url`, PUT, then `upload_s3_key`); importing creates a realm and refuses a name collision rather than overwriting, and needs `confirm=true` because it brings users and credentials with it.
Prompts
Eight prompts give you a starting point into that tool surface. Clients surface them as slash commands or suggested actions; each one takes arguments (realm, cluster, time window) and walks the model through the right tools in the right order.
| Prompt | What it does |
|---|---|
| `audit_self_registration` | Find every realm that still allows self-registration, across one cluster or all of them |
| `review_upgrades` | Spot clusters behind on their Keycloak version and lay out the upgrade path |
| `triage_failed_logins` | Pull recent failed logins for a realm and group them by source IP |
| `review_identity_providers` | List a realm's SSO connections and check whether a specific one is enabled |
| `review_admin_changes` | Show who changed what in a realm recently, focused on login and security settings |
| `provision_environment` | Create a cluster, add a realm, and wire up an identity provider, confirming each step |
| `set_up_custom_domain` | Add a custom domain, hand back the exact DNS records, verify, and route it to a realm |
| `rotate_client_secret` | Regenerate an application's client secret with the blast radius spelled out first |
Prompts are gated the same way as the tools they name: the three that mutate are only offered to sessions that could call the write tools they reference, and their instructions tell the model to confirm with you before changing anything. The `confirm=true` requirement on destructive tools still applies on top.
Skills
Where a prompt is a starting point, a skill is a full operational playbook the model loads on demand. The server ships four, served over the draft SEP-2640 Skills extension: it declares `io.modelcontextprotocol/skills` in its capabilities, answers `skills/list` and `skills/get`, and serves each `SKILL.md` as an ordinary resource at `skill:///SKILL.md` with a sha256 digest in its listing entry. OpenAI's plugin directory imports skills in exactly this shape.
| Skill | What it encodes |
|---|---|
| `auth-incident-triage` | Triage "users cannot log in": separate platform outages from attacks and from configuration changes, using events, WAF logs and cluster health. Read-only |
| `enterprise-sso-rollout` | Wire an enterprise IdP into a realm end to end: issuer validation, the upstream app registration, broker config, connection testing, and verification against real login events |
| `keycloak-migration-doctor` | Preflight a Keycloak export, import or migration against the blockers support actually sees (script policies, the legacy `/auth` path, partial-export expectations), and diagnose a failed job by reading its real `error_message` instead of the generic dashboard notice |
| `keycloak-upgrade-readiness` | Assess version drift, work out what the new Keycloak version breaks (extensions, themes), and sequence the rollout across environments with an export as the rollback plan |
Skills follow the same gating as the tools they name: the three workflows built around write tools are withheld from read-only sessions, and a scoped session is only offered a skill whose tools it actually has. The sources live in `internal/tools/skills/`, one directory per skill, in the standard Agent Skills format, so they also work copied straight into a local skills directory.
Connecting
For hosted HTTP the simplest route is OAuth, which needs no credential at all:
claude mcp add --transport http skycloak https://mcp.skycloak.ioThe first call opens your browser, you approve in the Skycloak login page, and the tools appear. If you belong to more than one workspace, name the one you want:
claude mcp add --transport http skycloak "https://mcp.skycloak.io?workspace="Otherwise, create an API key in the Skycloak dashboard and configure your MCP client to send it as a bearer token:
claude mcp add --transport http skycloak https://mcp.skycloak.io --header "Authorization: Bearer sk_sc_XXX"This adds the following to `.claude.json`:
{
"mcpServers": {
"skycloak": {
"type": "http",
"url": "https://mcp.skycloak.io",
"headers": {
"Authorization": "Bearer sk_sc_XXX"
}
}
}
}For local stdio, sign in once, then point your client at `skycloak-mcp run`:
skycloak-mcp init # one-time browser sign-in; stores a key in your keychainClaude Desktop / Cursor (local, stdio):
{
"mcpServers": {
"skycloak": {
"command": "skycloak-mcp",
"args": ["run", "--transport", "stdio"]
}
}
}Claude Code:
claude mcp add skycloak -- skycloak-mcp run --transport stdioFor headless / CI (no browser), skip `init` and pass the key instead: add `"env": { "SKYCLOAK_API_KEY": "sk_sc_..." }` to the config, or `claude mcp add skycloak --env SKYCLOAK_API_KEY=sk_sc_... -- skycloak-mcp run --transport stdio`.
Add `--allow-writes` only when you intend to make changes (sign in with `skycloak-mcp init --allow-writes`, or use a write-scoped key).
Add `?readonly=true` to a hosted HTTP URL to expose only read-only tools for that HTTP session, or `?readonly=false` to request the write-capable tool surface. The query parameter defaults to `false`, but write tools are registered only when the server was started with `--allow-writes`.
Add `?workspace=` to pick which workspace an OAuth session acts on. It is only needed when you belong to more than one; with a single workspace the server picks it for you, and if you belong to several and name none, the connection fails with a message listing them.
Running the HTTP transport
skycloak-mcp run --transport http --http-addr :8080It needs no credential of its own: callers supply theirs per request, so nothing is injected at deploy time. `GET /healthz` and `GET /readyz` are unauthenticated and report only that the process is up; they deliberately do not probe the Skycloak API, so an upstream blip cannot fail every replica's probe at once. The server holds no session state, so replicas need no session affinity and can be scaled or rolled freely. `SIGTERM` stops new connections and drains in-flight calls.
The OAuth path is on whenever `SKYCLOAK_ISSUER` and `SKYCLOAK_DASHBOARD_URL` are set, which they are by default. `GET /.well-known/oauth-protected-resource` is then served unauthenticated, naming the realm as the authorization server. Its `resource` value is taken from `SKYCLOAK_PUBLIC_URL` when set, and otherwise from the request's own `Host` and scheme, so a single-host deployment behind an ingress needs no extra configuration. The scheme comes from `X-Forwarded-Proto` when present, and otherwise defaults to `https` for anything but a loopback host, since TLS terminates upstream and publishing an `http://` identifier would not match the URL the client connected on. Set `SKYCLOAK_PUBLIC_URL` if your ingress rewrites `Host`. The document also lists `openid profile email` as its `scopes_supported`, and the `WWW-Authenticate` challenge repeats them as a `scope` parameter, so a client reading either one asks the realm for them: `openid` is required, because the token exchange makes the dashboard call Keycloak's userinfo endpoint and Keycloak refuses a token granted without it. A token that arrives without it is refused at verification with a `401` and the challenge, rather than carried to an exchange that cannot succeed, so a client still holding a grant from before stops retrying and signs in again. Blanking either of the issuer or dashboard variables turns OAuth off entirely, and the server goes back to challenging for an API key and nothing else.
`OPENAI_APPS_CHALLENGE_TOKEN` serves OpenAI's plugin-directory domain verification
token at `/.well-known/openai-apps-challenge`, as plain text and nothing else. Unset,
the route is not registered and the path 404s.
Startup logs one line with the wiring it resolved (`oauth=`, `issuer=`, `dashboard=`, `public_url=`, `endpoint=`, `allow_writes=`), so a misconfigured deployment can be spotted without a redeploy. Every request refused on the OAuth path logs one line naming the stage that failed (`verify`, `exchange` or `scopes`), the status the caller got, and the underlying error. A verification failure adds the check that rejected the token (`expired`, `wrong_issuer`, `bad_signature`, `unknown_key_id`, `wrong_token_type`, `no_openid_scope`, and so on); an exchange failure adds the dashboard's status and the host called. The caller appears as the token's subject once it is verified, and never as a credential: the access token, the `Authorization` header and the minted API key are never logged.
Configuration
| Env var | Default |
|---|---|
| `SKYCLOAK_API_KEY` | none (optional for stdio; HTTP clients provide `API-Key` headers instead) |
| `SKYCLOAK_ENDPOINT` | `https://api.skycloak.io` |
| `SKYCLOAK_API_VERSION` | current API version |
| `SKYCLOAK_ISSUER` | `https://login.app.skycloak.io/realms/skycloak` (CLI sign-in, and the authorization server the HTTP transport verifies tokens against) |
| `SKYCLOAK_CLIENT_ID` | `skycloak-mcp` (CLI device flow only) |
| `SKYCLOAK_DASHBOARD_URL` | `https://app.skycloak.io` (mints CLI keys and HTTP session keys) |
| `SKYCLOAK_PUBLIC_URL` | none (derived from each request; set it when the ingress rewrites `Host`) |
| `OPENAI_APPS_CHALLENGE_TOKEN` | Serves OpenAI's plugin-directory verification token at `/.well-known/openai-apps-challenge`. Unset, that path 404s. |
Commands: `init` (browser sign-in), `run` (serve), `logout` (remove the stored key). `init` accepts `--workspace `, `--allow-writes`, `--allow-credentials`, and `--ttl-days` (default 90).
| Flag | Default | Description |
|---|---|---|
| `--transport` | `stdio` | `stdio` or `http` |
| `--http-addr` | `:8080` | listen address for the HTTP transport |
| `--allow-writes` | `false` | enable mutating tools for stdio and permit HTTP sessions with `readonly=false` to register write tools |
Development
make build # build the server binary
make test # unit tests
make run # run on stdio for local testing
make inspector # MCP Inspector against the local binary
make lint # golangci-lint
make generate # regenerate the API client from the OpenAPI specThe API client under `internal/apiclient` is generated from the Skycloak OpenAPI
specification with oapi-codegen.
Keeping in sync with the API
The client in `internal/apiclient` is generated from `internal/apiclient/openapi.yaml` with oapi-codegen; run `make generate` to refresh it. CI fails if the committed generated code drifts from the spec. Requests are retried on `429`/`5xx` with `Retry-After`-aware backoff.
Distribution
Released as GitHub binaries and a `ghcr.io/sky-cloak/skycloak-mcp` container image on each tag, and published to the MCP Registry as `io.skycloak/skycloak-mcp`. Most people do not need either: the hosted server needs no install.
Security
Please report vulnerabilities privately. See SECURITY.md.
Contributors
Built at Skycloak by Guilliano Molaire, Neville Omangi and Aphilas. The repository history was
squashed when it was opened up, so the commit log does not reflect who wrote what.
License
Apache-2.0. The OpenAPI description in `internal/apiclient/openapi.yaml` is generated from the Skycloak
platform API and is (c) Skycloak; it is included here so the client can be generated and verified. See NOTICE.
Frequently asked questions
What is skycloak-mcp?
skycloak-mcp is Model Context Protocol server for Skycloak managed Keycloak. Manage clusters, realms, applications, SSO and users from any MCP client.
How do I install skycloak-mcp?
Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.
Is skycloak-mcp open source?
Yes — it is hosted on GitHub at https://github.com/sky-cloak/skycloak-mcp.
Related MCP tools
The missing open-source Kubernetes UI with a built-in MCP server for AI agents. See what's broken, why, and what changed. Issues, Topology, event timeline, Helm, GitOps, live service traffic, and cluster audits - all in one Go binary.
mcp-language-server gives MCP enabled clients access semantic tools like get definition, references, rename, and diagnostics.
One place to manage & connect to all your MCP servers
eBPF-powered network observability for Kubernetes. Indexes L4/L7 traffic with full K8s context, decrypts TLS without keys. Queryable by AI agents via MCP and humans via dashboard.
ToolHive makes deploying MCP servers easy, secure and fun Go-based implementation. Trusted by 1300+ developers. Trusted by 1300+ developers.
Open-source AI agent firewall for MCP security and agent egress. Scans mediated HTTP, MCP, A2A, and WebSocket traffic for exfiltration, SSRF, and prompt injection, and emits mediator-signed action receipts: verifiable audit evidence from outside the agent.
Run your own MCP server? See who uses it and what to fix.
Measure it with TrackMCP