Skip to main content
Glama
Faust-Systems

keycloak-mcp

README.md
# keycloak-mcp

An MCP (Model Context Protocol) server, stdio transport, that lets an AI
assistant inspect and — behind explicit guards — modify Keycloak realm,
client, and protocol-mapper configuration across **multiple Keycloak hosts**
from a single server process.

Built on the official
[`@keycloak/keycloak-admin-client`](https://www.npmjs.com/package/@keycloak/keycloak-admin-client)
and the [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk).
Design derived from the MIT-licensed
[Octodet keycloak-mcp](https://github.com/Octodet/keycloak-mcp) — see `NOTICE`.

## Install

```sh
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](#configuration)) and register the
server with your MCP client (see [Registering with Claude](#registering-with-claude-mcp-config)).

## Hosts

Every tool takes a `host` argument: a string key naming an entry in the
config file's `hosts` map (or discovered from the `KC_<HOST>_*` env
fallback — see below). Host keys are **entirely arbitrary and user-defined**
— there is no fixed list of hosts baked into this server. Point it at
whatever 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:

```json
"prod-eu": { "url": "...", "adminUser": "", "adminPassword": "", "production": true }
```

- `production: true` marks the host as production: writes against it are
  refused unless `allowProdWrite` is also `true` in the config.
- `production: false` marks it non-production: writes need only the
  per-call `write=true`.

> [!IMPORTANT]
> **Fail-closed rule:** if a host's `production` flag is **absent, `null`,
> or any non-boolean value**, the host is treated as **production**. Only a
> literal boolean `false` disarms the production write guard. This tool
> rotates live client secrets, so a typo or an omission in config must never
> silently 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 the
`admin-cli` client on the `master` realm, and caches the admin client
per host (re-authenticating when the token expires). The target realm is a
per-call argument on every tool.

## Safety model

- **Read-only by default.** Mutating tools (`ensure_hardcoded_claim_mapper`,
  `delete_protocol_mapper`) take a `write` boolean that defaults to `false`.
  Without `write=true` they return a dry-run plan and call no mutating API.
- **Production gate, fail-closed.** Even with `write=true`, a write against a
  production host is refused unless `allowProdWrite` is `true` in the config
  file (or, when using the env fallback, `KC_ALLOW_PROD_WRITE=true`). A host
  is production whenever its `production` flag is `true`, absent, or any
  non-boolean value — only an explicit `production: false` opts a host out of
  the gate. See [Hosts](#hosts).
- **No secrets, ever.** Admin passwords and tokens are never logged or
  returned; client secrets and registration access tokens are redacted from
  `get_client` output.
- **Audit trail.** Every successful write emits a structured JSON line to
  stderr: timestamp, host, realm, clientId, action, mapper name.
- **Idempotent verbs.** `ensure_hardcoded_claim_mapper` reconciles toward a
  desired state (already-present / update-with-diff / create) instead of
  blindly creating; genuine ambiguity (e.g. two mappers already emitting the
  claim) 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 secret
values. To keep those values **out of the AI model's context entirely**, the
server never returns or logs the value:

- The secret value is written **directly to a local file** by the server
  process (`outPath`, mode `0600`, exact bytes, no trailing newline).
- The tool returns **only metadata** — `{ host, realm, clientId, outPath,
  byteLength }` (plus `rotated: true` for a regeneration). The value is never
  in 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 generic
  message, so no response body can leak.
- The server warns (does not fail) if `outPath`'s directory is
  group/world-writable, since a secret file there could be exposed.
- `dump_client_secret` is a read (no Keycloak mutation) so it is **not**
  write-gated — but it still writes a local file. `regenerate_client_secret`
  **is** a mutation: `write=false` (default) is a dry run, and rotating a
  production host requires `allowProdWrite` (see the fail-closed rule under
  [Hosts](#hosts)).

## Configuration

All configuration — host URLs, admin credentials, and the production-write
gate — comes from a single JSON config file. Nothing is hardcoded.

### File location & precedence

The config path is resolved in this order:

1. The environment variable `KEYCLOAK_MCP_CONFIG`, if set (explicit override).
2. Otherwise the default `~/.config/keycloak-mcp/config.json`.

**The file takes precedence.** If no file is found at the resolved path, the
server falls back to the legacy per-host environment variables (below) so
nothing breaks — but when the file is present it is used exclusively and the
env vars are ignored.

### File shape

See `config.example.json`. Copy it to `~/.config/keycloak-mcp/config.json`
and fill in your own host keys and credentials:

```json
{
  "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 of
  hosts; add or remove entries freely.
- `production` (per host) gates writes behind `allowProdWrite`. **Fail-closed
  rule:** if `production` is absent, `null`, or any non-boolean value, the
  host is treated as production. Only a literal `production: false` opts a
  host out. See [Hosts](#hosts).
- `allowProdWrite` must be `true` to permit writes to any host whose
  `production` flag resolves to production (per the rule above); a host
  explicitly marked `production: false` only needs the per-call `write=true`.
- A host with a missing `url`, `adminUser`, or `adminPassword` fails only when
  addressed, with an error naming the missing **field names** (never values).
  The other hosts keep working.
- Calling a tool with a `host` key that isn't in the config fails with an
  error naming the requested host and listing the configured host **keys**
  (never values).

**Protect the file:** it holds admin credentials in the clear. Keep it at
`chmod 600` — the server prints a one-line warning to stderr (it does not
refuse) 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 variable
name, lowercased, with underscores left as-is: `KC_PROD_EU_URL` yields the
host key `prod_eu`. **Hyphens in a host key are not expressible via env
vars** — 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

```sh
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 needs
only `command` + `args` — **no `env` block**:

```json
{
  "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 it
at a different file, add a single override:

```json
"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 (configured
with `"production": false`, see [Hosts](#hosts)) must emit a hardcoded
`realm` claim with value `acme` in access tokens.

1. **Dry run** (default — nothing changes):

   `ensure_hardcoded_claim_mapper` with
   `{ "host": "staging", "realm": "acme", "clientId": "my-app", "claimName": "realm", "claimValue": "acme" }`

   → returns `status: "dry-run", wouldDo: "create"` with the exact mapper it
   would create — or `wouldDo: "update"` with a per-key diff if a mapper
   already exists with different config, or `status: "already-present"` if
   everything already matches.

2. **Apply**: same call plus `"write": true.`

   → creates/updates the mapper and emits an audit line to stderr.

3. Repeat against a production host (e.g. `prod-eu`) once verified — those
   calls additionally require `"allowProdWrite": true` in 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; see
`NOTICE` for attribution.

TDQS

A4.1/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: listing clients, getting one client, dumping/regenerating secrets, and managing protocol mappers (list, delete, ensure a specific type). No overlap or ambiguity.

Naming Consistency5/5

All tools follow a consistent verb_noun pattern in snake_case (e.g., list_clients, get_client, dump_client_secret). No mixing of conventions or unclear naming.

Tool Count5/5

7 tools is well-scoped for the focused domain of Keycloak client secrets and protocol mapper management. Not excessive, and each tool serves a clear purpose.

Completeness4/5

The tool set covers core operations for client secrets and protocol mappers, including reading, idempotent ensures, and deletions. Minor gaps exist (e.g., no generic protocol mapper creation or client CRUD), but the surface is coherent for its intended scope.

Maintenance

ActivityStale
ResponsivenessNo issues