keycloak-mcp
An MCP (Model Context Protocol) server, stdio transport, that lets an AIassistant inspect and — behind explicit guards — modify Keycloak realm,client, and protocol-mapper configuration across multiple Keycloak hostsfrom a single server process.
Built on the official@keycloak/keycloak-admin-clientand the @modelcontextprotocol/sdk.Design derived from the MIT-licensedOctodet keycloak-mcp — see NOTICE.
Install
git clone https://github.com/Faust-Systems/keycloak-mcp.git
cd keycloak-mcp
npm install
npm run build # tsc → dist/
Then create a config file (see Configuration) and register theserver with your MCP client (see Registering with Claude).
Hosts
Every tool takes a host argument: a string key naming an entry in theconfig file's hosts map (or discovered from the KC_<HOST>_* envfallback — see below). Host keys are entirely arbitrary and user-defined— there is no fixed list of hosts baked into this server. Point it atwhatever Keycloak instances you like, under whatever names make sense to you(prod-eu, staging, customer-a, ...).
Each host entry carries its own explicit production flag:
"prod-eu": { "url": "...", "adminUser": "", "adminPassword": "", "production": true }
production: truemarks the host as production: writes against it arerefused unlessallowProdWriteis alsotruein the config.production: falsemarks it non-production: writes need only theper-callwrite=true.
[!IMPORTANT]Fail-closed rule: if a host's
productionflag is absent,null,or any non-boolean value, the host is treated as production. Only aliteral booleanfalsedisarms the production write guard. This toolrotates live client secrets, so a typo or an omission in config must neversilently disable the safety gate — when in doubt, it defaults to the safer(more restrictive) behaviour.
The server authenticates per host with a password grant against theadmin-cli client on the master realm, and caches the admin clientper host (re-authenticating when the token expires). The target realm is aper-call argument on every tool.
Safety model
- Read-only by default. Mutating tools (
ensure_hardcoded_claim_mapper,delete_protocol_mapper) take awriteboolean that defaults tofalse.Withoutwrite=truethey return a dry-run plan and call no mutating API. - Production gate, fail-closed. Even with
write=true, a write against aproduction host is refused unlessallowProdWriteistruein the configfile (or, when using the env fallback,KC_ALLOW_PROD_WRITE=true). A hostis production whenever itsproductionflag istrue, absent, or anynon-boolean value — only an explicitproduction: falseopts a host out ofthe gate. See Hosts. - No secrets, ever. Admin passwords and tokens are never logged orreturned; client secrets and registration access tokens are redacted from
get_clientoutput. - Audit trail. Every successful write emits a structured JSON line tostderr: timestamp, host, realm, clientId, action, mapper name.
- Idempotent verbs.
ensure_hardcoded_claim_mapperreconciles toward adesired state (already-present / update-with-diff / create) instead ofblindly creating; genuine ambiguity (e.g. two mappers already emitting theclaim) is reported as a conflict and never auto-resolved.
Client secrets — the file-sink model
dump_client_secret and regenerate_client_secret handle real client secretvalues. To keep those values out of the AI model's context entirely, theserver never returns or logs the value:
- The secret value is written directly to a local file by the serverprocess (
outPath, mode0600, exact bytes, no trailing newline). - The tool returns only metadata —
{ host, realm, clientId, outPath, byteLength }(plusrotated: truefor a regeneration). The value is neverin the return object, any log line, the audit line, or any error message. - Keycloak admin-API failures are reduced to an HTTP status plus a genericmessage, so no response body can leak.
- The server warns (does not fail) if
outPath's directory isgroup/world-writable, since a secret file there could be exposed. dump_client_secretis a read (no Keycloak mutation) so it is notwrite-gated — but it still writes a local file.regenerate_client_secretis a mutation:write=false(default) is a dry run, and rotating aproduction host requiresallowProdWrite(see the fail-closed rule underHosts).
Configuration
All configuration — host URLs, admin credentials, and the production-writegate — comes from a single JSON config file. Nothing is hardcoded.
File location & precedence
The config path is resolved in this order:
- The environment variable
KEYCLOAK_MCP_CONFIG, if set (explicit override). - Otherwise the default
~/.config/keycloak-mcp/config.json.
The file takes precedence. If no file is found at the resolved path, theserver falls back to the legacy per-host environment variables (below) sonothing breaks — but when the file is present it is used exclusively and theenv vars are ignored.
File shape
See config.example.json. Copy it to ~/.config/keycloak-mcp/config.jsonand fill in your own host keys and credentials:
{
"hosts": {
"prod-eu": { "url": "https://keycloak.example.com", "adminUser": "", "adminPassword": "", "production": true },
"staging": { "url": "https://kc-staging.example.com", "adminUser": "", "adminPassword": "", "production": false }
},
"allowProdWrite": false
}
- Host keys (
"prod-eu","staging"above) are entirely arbitrary —name your hosts however makes sense to you. There is no fixed set ofhosts; add or remove entries freely. production(per host) gates writes behindallowProdWrite. Fail-closedrule: ifproductionis absent,null, or any non-boolean value, thehost is treated as production. Only a literalproduction: falseopts ahost out. See Hosts.allowProdWritemust betrueto permit writes to any host whoseproductionflag resolves to production (per the rule above); a hostexplicitly markedproduction: falseonly needs the per-callwrite=true.- A host with a missing
url,adminUser, oradminPasswordfails only whenaddressed, with an error naming the missing field names (never values).The other hosts keep working. - Calling a tool with a
hostkey that isn't in the config fails with anerror naming the requested host and listing the configured host keys(never values).
Protect the file: it holds admin credentials in the clear. Keep it atchmod 600 — the server prints a one-line warning to stderr (it does notrefuse) if the file is group- or world-readable.
Environment-variable fallback
Used only when no config file exists at the resolved path. See .env.example.
Host keys are discovered by scanning for a KC_<HOST>_URL variable —there is no fixed list. The host key is the captured part of the variablename, lowercased, with underscores left as-is: KC_PROD_EU_URL yields thehost key prod_eu. Hyphens in a host key are not expressible via envvars — use the config file if you need one.
| variable | purpose |
|---|---|
KEYCLOAK_MCP_CONFIG |
override the config file path (else ~/.config/keycloak-mcp/config.json) |
KC_<HOST>_URL |
the host's base URL — presence of this variable is what discovers the host |
KC_<HOST>_ADMIN_USER / KC_<HOST>_ADMIN_PASSWORD |
admin credentials for that host |
KC_<HOST>_PRODUCTION |
false (case-insensitive) marks the host non-production; absent or any other value fails closed to production |
KC_ALLOW_PROD_WRITE |
true to allow writes to production hosts (fallback for allowProdWrite) |
Build
npm install
npm run build # tsc → dist/
npm test # vitest unit tests
Registering with Claude (MCP config)
Because all credentials live in the config file, the MCP registration needsonly command + args — no env block:
{
"mcpServers": {
"keycloak": {
"command": "node",
"args": ["/absolute/path/to/keycloak-mcp/dist/index.js"]
}
}
}
The server reads ~/.config/keycloak-mcp/config.json by default. To point itat a different file, add a single override:
"env": { "KEYCLOAK_MCP_CONFIG": "/absolute/path/to/config.json" }
Keep allowProdWrite at false in the file to hold production read-only;set it to true only when a session must change production.
Tools
| tool | arguments | what it does |
|---|---|---|
list_clients |
host, realm | clients of a realm: {clientId, id, name} |
get_client |
host, realm, clientId | full client representation (secrets redacted) |
list_protocol_mappers |
host, realm, clientId | the client's dedicated-scope mappers: id, name, type, config |
ensure_hardcoded_claim_mapper |
host, realm, clientId, claimName, claimValue, addToAccessToken=true, addToIdToken=false, claimJsonType="String", write=false | idempotently ensure an oidc-hardcoded-claim-mapper emits the claim |
delete_protocol_mapper |
host, realm, clientId, mapperId, write=false | delete one mapper by UUID |
dump_client_secret |
host, realm, clientId, outPath | write the client's current secret to a local 0600 file; returns metadata only |
regenerate_client_secret |
host, realm, clientId, outPath, write=false | rotate the client secret and write the new value to a local 0600 file; returns metadata only |
Usage example — hardcoded realm claim
Goal: the my-app client in realm acme on the staging host (configuredwith "production": false, see Hosts) must emit a hardcodedrealm claim with value acme in access tokens.
Dry run (default — nothing changes):
ensure_hardcoded_claim_mapperwith{ "host": "staging", "realm": "acme", "clientId": "my-app", "claimName": "realm", "claimValue": "acme" }→ returns
status: "dry-run", wouldDo: "create"with the exact mapper itwould create — orwouldDo: "update"with a per-key diff if a mapperalready exists with different config, orstatus: "already-present"ifeverything already matches.Apply: same call plus
"write": true.→ creates/updates the mapper and emits an audit line to stderr.
Repeat against a production host (e.g.
prod-eu) once verified — thosecalls additionally require"allowProdWrite": truein the config file.
Development
src/index.ts— server bootstrap + tool registration (stdio).src/config.ts— config file loader (file → env fallback), perms warning.src/hosts.ts— host registry, config validation, production write gate.src/kc.ts— cached, re-authenticating admin client per host.src/mappers.ts— pure plan/diff logic for the ensure tool (unit-tested).src/secrets.ts— client-secret file-sink tools (value never returned/logged).src/redact.ts— secret redaction.src/audit.ts— stderr audit lines.test/— vitest unit tests for all pure logic.
License
MIT — see LICENSE. Derived from the design of Octodet's keycloak-mcp; seeNOTICE for attribution.