About Keycloak Admin API
An unofficial Keycloak Admin and Account API package that also includes a stdio MCP server. It is intended for developers and operators who need typed API access or agent-based realm administration.
Key features
- 01Generated typed Admin and Account API clients
- 02MCP tool for each realm admin operation
- 03Read-only tools by default
- 04Enable write tools with an environment variable
- 05Restrict tools to selected resources
Use cases
- 01Inspect users, groups, and roles in a realm
- 02Let an agent query Keycloak administration data
- 03Limit available tools to chosen resources
- 04Enable selected data-changing operations
MCP support
Ships a stdio MCP server exposing realm-scoped admin operations as tools. It lists GET tools by default; setting KEYCLOAK_MCP_ALLOW_WRITES=1 adds write operations.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
npx -y keycloak-apiKEYCLOAK_URLSecretBase URL of the server, such as https://keycloak.example.comKEYCLOAK_REALMSecretRealm the tools act onKEYCLOAK_TOKENSecretA fixed access token; otherwise set KEYCLOAK_CLIENT_ID and KEYCLOAK_CLIENT_SECRETKEYCLOAK_CLIENT_IDSecretA confidential client with a service account, for the client credentials grantKEYCLOAK_CLIENT_SECRETSecretSecret of that clientKEYCLOAK_TOKEN_REALMSecretRealm of that client, if not KEYCLOAK_REALM (for example master)KEYCLOAK_MCP_ALLOW_WRITESSecret1 to list the tools that change dataKEYCLOAK_MCP_RESOURCESSecretComma separated resources to list tools for, such as users,groups,rolesLocal servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.
Tools
Local servers expose tools only once running on your machine, and the maintainer has not provided a tool list.
Details
From the project READMEkeycloak-api
Unofficial. Not made or endorsed by Keycloak.
Auto-generated, fully typed client for the Keycloak Admin and Account REST APIs.
Generated from the admin OpenAPI spec at https://www.keycloak.org/docs-api/latest/rest-api/openapi.json and the account spec kept in this repository at https://raw.githubusercontent.com/sferarc/openapi/main/packages/keycloak-api/specs/account.yaml; before generating, kubb.config.ts renames operation IDs, camel-cases path parameters, sends raw string bodies unquoted and types the workflow resource path parameter as its enum.
Features
- Fully typed - every endpoint, parameter and response is generated from the official OpenAPI specs
- Admin and Account APIs - separate clients for the two Keycloak surfaces
- Tree-shakable - only the operations you import end up in your bundle
- Runtime agnostic - uses the global
fetch, or bring your own implementation - Effect support - optional Effect bindings for the Admin API via
keycloak-api/effect
Installation
npm install keycloak-api
Quick Start
Keycloak is self-hosted, so both clients require the baseUrl of your server. Endpoints are addressed
as "<METHOD> <path>".
src/admin/generated/OPERATIONS.md and src/account/generated/OPERATIONS.md list every admin and account operation with its "<METHOD> <path>" request and the spec's summary, one table per tag. They ship in the package, so grep in node_modules/keycloak-api finds an operation without reading the types.
Admin API
import { KeycloakAdminApi } from "keycloak-api";
const admin = new KeycloakAdminApi({
baseUrl: "https://keycloak.example.com",
token: process.env.KEYCLOAK_TOKEN ?? null,
});
const realms = await admin.request("GET /admin/realms");
const realm = await admin.request("GET /admin/realms/{realm}", {
pathParams: { realm: "master" },
});
Account API
The Account API is scoped to a single realm, passed at construction time.
import { KeycloakAccountApi } from "keycloak-api";
const account = new KeycloakAccountApi({
baseUrl: "https://keycloak.example.com",
realm: "master",
token: process.env.KEYCLOAK_TOKEN ?? null,
});
const me = await account.request("GET /account/");
Refreshing the token
Keycloak access tokens expire after a few minutes. Pass a function as token, sync or async, and both clients call it before every request, so each call sends the token it returns:
const admin = new KeycloakAdminApi({
baseUrl: "https://keycloak.example.com",
token: () => tokenStore.getAccessToken(),
});
A call rejects with what the function threw, before any request is sent. A call's own config.token still wins.
Custom fetch
const admin = new KeycloakAdminApi({
baseUrl: "https://keycloak.example.com",
token: process.env.KEYCLOAK_TOKEN ?? null,
fetch: myFetchImplementation,
});
Response headers
A call resolves to the parsed body only. To read a response header, pass onResponse in config; it gets the status and headers of the response before the body is read, an error response included. Keycloak answers a create call, such as a new user or client, with 201, an empty body and the new resource's URL, which ends in its id, in Location:
let userId: string | undefined;
await admin.request("POST /admin/realms/{realm}/users", {
pathParams: { realm: "master" },
body: { username: "ada", enabled: true },
config: { onResponse: ({ headers }) => (userId = headers.get("Location")?.split("/").pop()) },
});
An error thrown inside onResponse rejects the call with an Error whose message starts with onResponse threw on and whose cause is what was thrown.
Errors
A call that gets an HTTP error response rejects with an ApiResponseError, exported from the package, which extends Error: status is the HTTP status code, payload is the parsed JSON error body, or the raw body text, up to 1000 characters, when it is not JSON, and method and path name the request, without its query string. Its message reads <METHOD> <path> returned <status>: <detail>, where the detail is the API's own error message or else the first 200 characters of the body. A request that gets no response at all, for example because the network failed, rejects with an Error whose message names the method, the path and the reason, and whose cause is the original exception. A request cancelled through signal rejects with what the signal was aborted with, as is: the AbortError, or the TimeoutError of AbortSignal.timeout().
Effect
The effect entrypoint exposes every Admin API operation as an Effect, by tag in AdminApiService and by "<METHOD> <path>" in adminEffectByPath, and every Account API operation in AccountApiService and accountEffectByPath. It requires effect 4 as a peer dependency. As with the client class, the layer needs the baseUrl of your server, and token can be a function that returns a fresh token before every call.
import { Effect } from "effect";
import { adminEffectByPath, makeApiLayer } from "keycloak-api/effect";
const program = adminEffectByPath["GET /admin/realms/{realm}"]({ pathParams: { realm: "master" } });
await Effect.runPromise(
program.pipe(
Effect.provide(
makeApiLayer({
baseUrl: "https://keycloak.example.com",
token: () => tokenStore.getAccessToken(),
})
)
)
);
Errors are typed as ApiError, NetworkError and ValidationError. An HTTP error response fails with ApiError, which carries the HTTP status; a request that never gets a response, or a token function that throws, fails with NetworkError.
A call can pass its own config, which layers over the layer's: its headers join the layer's, and its other fields, such as onResponse, replace them. An onResponse that throws makes the effect die with the onResponse threw on error as a defect rather than fail with NetworkError.
Account API paths sit under a realm, so an account effect also needs makeAccountLayer(realm), which rebases the server layer's baseUrl onto /realms/<realm> and encodes the realm as KeycloakAccountApi does. One makeApiLayer then serves admin and account effects in the same program:
import { Effect } from "effect";
import { AccountApiService, AdminApiService, makeAccountLayer, makeApiLayer } from "keycloak-api/effect";
const program = Effect.gen(function* () {
const realms = yield* AdminApiService.realmsAdmin.gETAdminRealms();
const account = yield* AccountApiService.account
.getAccount()
.pipe(Effect.provide(makeAccountLayer("my realm")));
return { realms, account };
});
await Effect.runPromise(
program.pipe(
Effect.provide(
makeApiLayer({
baseUrl: "https://keycloak.example.com",
token: () => tokenStore.getAccessToken(),
})
)
)
);
An account effect run without makeAccountLayer goes to <baseUrl>/account/, which is not a Keycloak endpoint.
MCP server
The package ships keycloak-api-mcp, a stdio MCP server with one tool per admin operation on a realm, so an agent can answer questions such as which users of a realm hold a role. It lists only the read (GET) tools; set KEYCLOAK_MCP_ALLOW_WRITES=1 to add the others, which carry destructiveHint on DELETE. Every tool acts on KEYCLOAK_REALM, which is not a tool argument: the two operations that list and import realms are left out, and a call whose URL resolves outside that realm, such as a group path with .. segments, is refused as a tool error. An HTTP error comes back as a tool error with the status and Keycloak's message.
| Variable | |
|---|---|
KEYCLOAK_URL | Base URL of the server, such as https://keycloak.example.com |
KEYCLOAK_REALM | Realm the tools act on |
KEYCLOAK_TOKEN | A fixed access token, or else: |
KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET | A confidential client with a service account; the server gets a token with the client credentials grant and renews it before it expires |
KEYCLOAK_TOKEN_REALM | Realm of that client, if not KEYCLOAK_REALM (for example master) |
KEYCLOAK_MCP_ALLOW_WRITES | 1 to list the tools that change data |
KEYCLOAK_MCP_RESOURCES | Comma separated resources to list tools for, such as users,groups,roles: the first path segment after the realm, or realm for the realm itself. Unset lists every resource |
The service account needs the realm-management roles for what the tools should read, such as view-users.
Without KEYCLOAK_MCP_RESOURCES the server lists about 200 read tools, over the 128 a VS Code chat request can enable, so pick the resources there. An unknown name stops the server with the list of valid ones.
Claude Code, in .mcp.json:
{
"mcpServers": {
"keycloak": {
"command": "npx",
"args": ["-y", "-p", "keycloak-api", "keycloak-api-mcp"],
"env": {
"KEYCLOAK_URL": "https://keycloak.example.com",
"KEYCLOAK_REALM": "acme",
"KEYCLOAK_CLIENT_ID": "mcp",
"KEYCLOAK_CLIENT_SECRET": "${KEYCLOAK_CLIENT_SECRET}"
}
}
}
}
VS Code, in .vscode/mcp.json:
{
"inputs": [
{ "type": "promptString", "id": "keycloak-secret", "description": "Keycloak client secret", "password": true }
],
"servers": {
"keycloak": {
"type": "stdio",
"command": "npx",
"args": ["-y", "-p", "keycloak-api", "keycloak-api-mcp"],
"env": {
"KEYCLOAK_URL": "https://keycloak.example.com",
"KEYCLOAK_REALM": "acme",
"KEYCLOAK_CLIENT_ID": "mcp",
"KEYCLOAK_CLIENT_SECRET": "${input:keycloak-secret}",
"KEYCLOAK_MCP_RESOURCES": "users,groups,roles"
}
}
}
}
Entrypoints
| Import | Contents |
|---|---|
keycloak-api | both client classes, plus admin/account operations, schemas and types |
keycloak-api/admin/components | admin operations grouped by tag and by path |
keycloak-api/admin/schemas | Zod schemas for admin operations |
keycloak-api/admin/types | TypeScript types for admin operations |
keycloak-api/account/components | account operations grouped by tag and by path |
keycloak-api/account/schemas | Zod schemas for account operations |
keycloak-api/account/types | TypeScript types for account operations |
keycloak-api/effect | Effect bindings for admin operations |
License
ISC
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.