keycloak-model-context-protocol 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -101
- package/dist/config.js +178 -0
- package/dist/config.js.map +1 -0
- package/dist/guards.js +47 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.js +36 -174
- package/dist/index.js.map +1 -0
- package/dist/keycloak/errors.js +86 -0
- package/dist/keycloak/errors.js.map +1 -0
- package/dist/keycloak/gateway.js +131 -0
- package/dist/keycloak/gateway.js.map +1 -0
- package/dist/logging.js +49 -0
- package/dist/logging.js.map +1 -0
- package/dist/prompts/audit-realm-security.js +26 -0
- package/dist/prompts/audit-realm-security.js.map +1 -0
- package/dist/prompts/index.js +12 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/investigate-failed-logins.js +34 -0
- package/dist/prompts/investigate-failed-logins.js.map +1 -0
- package/dist/prompts/offboard-employee.js +31 -0
- package/dist/prompts/offboard-employee.js.map +1 -0
- package/dist/prompts/onboard-employee.js +50 -0
- package/dist/prompts/onboard-employee.js.map +1 -0
- package/dist/prompts/shared.js +46 -0
- package/dist/prompts/shared.js.map +1 -0
- package/dist/resources/index.js +8 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/realms.js +96 -0
- package/dist/resources/realms.js.map +1 -0
- package/dist/resources/shared.js +44 -0
- package/dist/resources/shared.js.map +1 -0
- package/dist/resources/users.js +26 -0
- package/dist/resources/users.js.map +1 -0
- package/dist/server.js +16 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/clients.js +137 -0
- package/dist/tools/clients.js.map +1 -0
- package/dist/tools/events.js +161 -0
- package/dist/tools/events.js.map +1 -0
- package/dist/tools/groups.js +135 -0
- package/dist/tools/groups.js.map +1 -0
- package/dist/tools/index.js +31 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/realms.js +99 -0
- package/dist/tools/realms.js.map +1 -0
- package/dist/tools/roles.js +270 -0
- package/dist/tools/roles.js.map +1 -0
- package/dist/tools/security.js +58 -0
- package/dist/tools/security.js.map +1 -0
- package/dist/tools/sessions.js +68 -0
- package/dist/tools/sessions.js.map +1 -0
- package/dist/tools/shared.js +144 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/users.js +272 -0
- package/dist/tools/users.js.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +46 -11
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Christoph Englisch
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,42 +1,102 @@
|
|
|
1
1
|
# Keycloak MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/keycloak-model-context-protocol)
|
|
4
|
+
[](https://github.com/ChristophEnglisch/keycloak-model-context-protocol/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for **Keycloak administration**. It lets an AI assistant such as Claude manage users, groups, roles and sessions, and inspect clients, events and the security posture of your realms, through the Keycloak Admin REST API, with least-privilege authentication and guardrails around every write.
|
|
8
|
+
|
|
9
|
+
> [!WARNING]
|
|
10
|
+
> This server acts with the permissions of the Keycloak account you give it. Use a **dedicated service-account client with only the roles you need**, never the bootstrap admin. Destructive tools ask for confirmation by default, and a read-only mode is available. See [Security](#security).
|
|
11
|
+
|
|
12
|
+
## Highlights
|
|
13
|
+
|
|
14
|
+
- **24 tools in 7 toolsets**, enabled per toolset. The default set (`core`, `groups`, `roles`) is small; everything else is opt-in.
|
|
15
|
+
- **Least-privilege auth.** `client_credentials` with a service account is the recommended grant; the password grant stays available. No credential defaults, so a missing setting fails fast.
|
|
16
|
+
- **Guardrails.** Read-only mode, realm allowlist, write protection for the `master` realm, and a preview-then-`confirm: true` flow for destructive operations.
|
|
17
|
+
- **4 guided prompts** (onboarding, offboarding, security audit, failed-login investigation) and **MCP resources** for realms and users, with realm-name completion.
|
|
18
|
+
- **Structured, paginated output** with output schemas; secrets and credentials are never returned or logged.
|
|
19
|
+
- Works with Claude Desktop, Claude Code, VS Code, Cursor and any stdio MCP client. Requires Node.js 22 or newer.
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
Your MCP client starts the server as a local child process and talks to it over stdio. Every tool call passes the guards, then goes through a single gateway that owns the Keycloak login and caches the access token.
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
flowchart LR
|
|
27
|
+
subgraph Host["Your machine"]
|
|
28
|
+
Client["MCP client<br/>(Claude Desktop, Claude Code,<br/>VS Code, Cursor, ...)"]
|
|
29
|
+
subgraph Server["keycloak-model-context-protocol (Node.js)"]
|
|
30
|
+
Guards["Guards<br/>read-only, realm allowlist,<br/>master protection, confirmation"]
|
|
31
|
+
Toolsets["Toolsets, prompts, resources<br/>core, groups, roles, clients,<br/>sessions, events, security"]
|
|
32
|
+
Gateway["Gateway<br/>login and token cache"]
|
|
33
|
+
Guards --> Toolsets --> Gateway
|
|
34
|
+
end
|
|
35
|
+
Client <-->|"JSON-RPC over stdio"| Guards
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
subgraph KC["Keycloak"]
|
|
39
|
+
Token["Token endpoint"]
|
|
40
|
+
Admin["Admin REST API<br/>/admin/realms/..."]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Gateway -->|"client_credentials or password grant"| Token
|
|
44
|
+
Gateway -->|"Bearer token"| Admin
|
|
45
|
+
```
|
|
4
46
|
|
|
5
|
-
|
|
47
|
+
### What a destructive call looks like
|
|
48
|
+
|
|
49
|
+
A destructive tool never acts on the first call. It returns a preview of what would happen and changes nothing. Only after you approve does the assistant repeat the call with `confirm: true`.
|
|
50
|
+
|
|
51
|
+
```mermaid
|
|
52
|
+
sequenceDiagram
|
|
53
|
+
autonumber
|
|
54
|
+
actor User
|
|
55
|
+
participant LLM as MCP client / LLM
|
|
56
|
+
participant MCP as Keycloak MCP server
|
|
57
|
+
participant KC as Keycloak
|
|
58
|
+
|
|
59
|
+
User->>LLM: "Delete user bob from realm demo"
|
|
60
|
+
LLM->>MCP: user_delete (realm, userId)
|
|
61
|
+
MCP->>KC: GET user (for the preview)
|
|
62
|
+
KC-->>MCP: user data
|
|
63
|
+
MCP-->>LLM: isError, "Confirmation required. This would: permanently delete user bob ... Nothing was changed."
|
|
64
|
+
LLM->>User: "This will permanently delete bob. Proceed?"
|
|
65
|
+
User->>LLM: "Yes"
|
|
66
|
+
LLM->>MCP: user_delete (realm, userId, confirm: true)
|
|
67
|
+
MCP->>KC: DELETE user
|
|
68
|
+
KC-->>MCP: 204 No Content
|
|
69
|
+
MCP-->>LLM: deleted: true
|
|
70
|
+
LLM-->>User: "User bob was deleted."
|
|
71
|
+
```
|
|
6
72
|
|
|
7
|
-
|
|
8
|
-
- Delete users from realms
|
|
9
|
-
- List available realms
|
|
10
|
-
- List users in specific realms
|
|
73
|
+
## Quick start
|
|
11
74
|
|
|
12
|
-
|
|
75
|
+
**Requirements:** Node.js 22 or newer, a Keycloak server your machine can reach (tested with Keycloak 24.0 to 26.8), and an MCP client.
|
|
13
76
|
|
|
14
|
-
###
|
|
77
|
+
### 1. Prepare Keycloak (3 steps)
|
|
15
78
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
79
|
+
In the realm you want to administer (here `demo`):
|
|
80
|
+
1. **Create a client** `mcp-admin` with _Client authentication_ **on** and only _Service accounts roles_ enabled (turn off _Standard flow_ and _Direct access grants_).
|
|
81
|
+
2. **Assign roles.** On the client's _Service accounts roles_ tab, assign client roles of `realm-management`. For a first read-only trial: `view-users`, `query-users`, `query-groups`, `view-realm`.
|
|
82
|
+
3. **Copy the secret** from the client's _Credentials_ tab.
|
|
20
83
|
|
|
21
|
-
|
|
22
|
-
npm install -g keycloak-model-context-protocol
|
|
23
|
-
```
|
|
84
|
+
Step-by-step instructions with Admin Console click paths, `kcadm.sh` commands and a local Docker Keycloak: [docs/getting-started.md](docs/getting-started.md).
|
|
24
85
|
|
|
25
|
-
###
|
|
86
|
+
### 2. Configure your MCP client
|
|
26
87
|
|
|
27
|
-
|
|
88
|
+
Example for Claude Code (other clients: [docs/clients.md](docs/clients.md)):
|
|
28
89
|
|
|
29
90
|
```bash
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
91
|
+
claude mcp add --transport stdio \
|
|
92
|
+
--env KEYCLOAK_URL=https://keycloak.example.com \
|
|
93
|
+
--env KEYCLOAK_AUTH_REALM=demo \
|
|
94
|
+
--env KEYCLOAK_CLIENT_ID=mcp-admin \
|
|
95
|
+
--env KEYCLOAK_CLIENT_SECRET="$KEYCLOAK_CLIENT_SECRET" \
|
|
96
|
+
keycloak -- npx -y keycloak-model-context-protocol
|
|
34
97
|
```
|
|
35
98
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
### Using NPM Package (Recommended)
|
|
39
|
-
Configure the server in your Claude Desktop configuration file:
|
|
99
|
+
Or as JSON (Claude Desktop, `claude_desktop_config.json`):
|
|
40
100
|
|
|
41
101
|
```json
|
|
42
102
|
{
|
|
@@ -45,89 +105,116 @@ Configure the server in your Claude Desktop configuration file:
|
|
|
45
105
|
"command": "npx",
|
|
46
106
|
"args": ["-y", "keycloak-model-context-protocol"],
|
|
47
107
|
"env": {
|
|
48
|
-
"KEYCLOAK_URL": "
|
|
49
|
-
"
|
|
50
|
-
"
|
|
108
|
+
"KEYCLOAK_URL": "https://keycloak.example.com",
|
|
109
|
+
"KEYCLOAK_AUTH_REALM": "demo",
|
|
110
|
+
"KEYCLOAK_CLIENT_ID": "mcp-admin",
|
|
111
|
+
"KEYCLOAK_CLIENT_SECRET": "<client-secret>"
|
|
51
112
|
}
|
|
52
113
|
}
|
|
53
114
|
}
|
|
54
115
|
}
|
|
55
116
|
```
|
|
56
117
|
|
|
57
|
-
###
|
|
58
|
-
```json
|
|
59
|
-
{
|
|
60
|
-
"mcpServers": {
|
|
61
|
-
"keycloak": {
|
|
62
|
-
"command": "node",
|
|
63
|
-
"args": ["path/to/dist/index.js"],
|
|
64
|
-
"env": {
|
|
65
|
-
"KEYCLOAK_URL": "http://localhost:8080",
|
|
66
|
-
"KEYCLOAK_ADMIN": "admin",
|
|
67
|
-
"KEYCLOAK_ADMIN_PASSWORD": "admin"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Available Tools
|
|
75
|
-
|
|
76
|
-
### create-user
|
|
77
|
-
Creates a new user in a specified realm.
|
|
78
|
-
|
|
79
|
-
**Inputs**:
|
|
80
|
-
- `realm`: The realm name
|
|
81
|
-
- `username`: Username for the new user
|
|
82
|
-
- `email`: Email address for the user
|
|
83
|
-
- `firstName`: User's first name
|
|
84
|
-
- `lastName`: User's last name
|
|
118
|
+
### 3. Ask your first question
|
|
85
119
|
|
|
86
|
-
|
|
87
|
-
Deletes a user from a specified realm.
|
|
120
|
+
> _List the realms you can access, then show me the first 10 users of realm demo._
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
- `realm`: The realm name
|
|
91
|
-
- `userId`: The ID of the user to delete
|
|
122
|
+
If you get an answer, you are done. If not, see [docs/troubleshooting.md](docs/troubleshooting.md).
|
|
92
123
|
|
|
93
|
-
|
|
94
|
-
Lists all available realms.
|
|
95
|
-
|
|
96
|
-
### list-users
|
|
97
|
-
Lists all users in a specified realm.
|
|
98
|
-
|
|
99
|
-
**Inputs**:
|
|
100
|
-
- `realm`: The realm name
|
|
101
|
-
|
|
102
|
-
## Development
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
npm run watch
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Testing
|
|
109
|
-
|
|
110
|
-
To test the server using MCP Inspector:
|
|
111
|
-
|
|
112
|
-
```bash
|
|
113
|
-
npx -y @modelcontextprotocol/inspector npx -y keycloak-model-context-protocol
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Deployment
|
|
117
|
-
|
|
118
|
-
### NPM Package
|
|
119
|
-
|
|
120
|
-
This project is automatically published to [NPM](https://www.npmjs.com/package/keycloak-model-context-protocol) via GitHub Actions when a new release is published on GitHub.
|
|
121
|
-
|
|
122
|
-
#### Setup Requirements for Deployment
|
|
123
|
-
|
|
124
|
-
1. Create NPM account and get access token
|
|
125
|
-
2. Add NPM_TOKEN secret to GitHub repository
|
|
126
|
-
- Go to repository Settings > Secrets
|
|
127
|
-
- Add new secret named `NPM_TOKEN`
|
|
128
|
-
- Paste your NPM access token as the value
|
|
129
|
-
|
|
130
|
-
## Prerequisites
|
|
124
|
+
## Configuration
|
|
131
125
|
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
All settings are environment variables. Only `KEYCLOAK_URL` and one set of credentials are required.
|
|
127
|
+
|
|
128
|
+
| Variable | Default | Purpose |
|
|
129
|
+
| ----------------------------------------------- | ------------------- | -------------------------------------------------------------- |
|
|
130
|
+
| `KEYCLOAK_URL` | (required) | Base URL of the Keycloak server |
|
|
131
|
+
| `KEYCLOAK_AUTH_REALM` | `master` | Realm where the login account or client lives |
|
|
132
|
+
| `KEYCLOAK_CLIENT_ID` / `KEYCLOAK_CLIENT_SECRET` | none | `client_credentials` login (recommended) |
|
|
133
|
+
| `KEYCLOAK_USERNAME` / `KEYCLOAK_PASSWORD` | none | Password grant login (client defaults to `admin-cli`) |
|
|
134
|
+
| `KEYCLOAK_GRANT_TYPE` | auto | `client_credentials` or `password`; detected from what you set |
|
|
135
|
+
| `KEYCLOAK_MCP_TOOLSETS` | `core,groups,roles` | Enabled toolsets, or `all` |
|
|
136
|
+
| `KEYCLOAK_MCP_READ_ONLY` | `false` | Register read-only tools only |
|
|
137
|
+
| `KEYCLOAK_MCP_REALMS` | all | Comma-separated allowlist of target realms |
|
|
138
|
+
| `KEYCLOAK_MCP_ALLOW_MASTER_WRITE` | `false` | Allow write tools in the `master` realm |
|
|
139
|
+
| `KEYCLOAK_MCP_REQUIRE_CONFIRMATION` | `true` | Destructive tools need `confirm: true` |
|
|
140
|
+
| `KEYCLOAK_MCP_TIMEOUT_MS` | `30000` | HTTP timeout for Keycloak requests |
|
|
141
|
+
| `KEYCLOAK_MCP_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error` (stderr) |
|
|
142
|
+
|
|
143
|
+
Full reference with examples, validation rules and the tool-to-role matrix: [docs/configuration.md](docs/configuration.md).
|
|
144
|
+
|
|
145
|
+
## Toolsets and tools
|
|
146
|
+
|
|
147
|
+
Tools are named `<domain>_<verb>`. Only the **default** toolsets are enabled unless you set `KEYCLOAK_MCP_TOOLSETS`. In read-only mode only tools of kind _read_ are registered.
|
|
148
|
+
|
|
149
|
+
| Toolset | Default | Tools |
|
|
150
|
+
| ---------- | :-----: | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
151
|
+
| `core` | yes | `realm_list`, `realm_get`, `user_search`, `user_get`, `user_create`, `user_update`, `user_delete` (destructive), `user_send_actions_email` |
|
|
152
|
+
| `groups` | yes | `group_list`, `group_members_list`, `user_group_add`, `user_group_remove` (destructive) |
|
|
153
|
+
| `roles` | yes | `role_list`, `user_roles_list`, `user_role_assign` (confirmation for admin roles), `user_role_revoke` (destructive) |
|
|
154
|
+
| `clients` | no | `client_list`, `client_get` |
|
|
155
|
+
| `sessions` | no | `user_sessions_list`, `user_logout` (destructive) |
|
|
156
|
+
| `events` | no | `event_list`, `admin_event_list` |
|
|
157
|
+
| `security` | no | `user_brute_force_status`, `user_brute_force_unlock` |
|
|
158
|
+
|
|
159
|
+
Details for every tool (parameters, outputs, required roles): [docs/tools-reference.md](docs/tools-reference.md).
|
|
160
|
+
|
|
161
|
+
## Prompts and resources
|
|
162
|
+
|
|
163
|
+
**Prompts** are guided workflows built on the tools. They appear in your client's prompt picker (in Claude Code as slash commands). The `realm` argument autocompletes.
|
|
164
|
+
|
|
165
|
+
| Prompt | What it does |
|
|
166
|
+
| --------------------------- | --------------------------------------------------------------------------------- |
|
|
167
|
+
| `onboard_employee` | Create a user, add groups and roles, send the welcome e-mail |
|
|
168
|
+
| `offboard_employee` | Review, end sessions and disable a departing employee's account (does not delete) |
|
|
169
|
+
| `audit_realm_security` | Read-only security review with prioritized findings |
|
|
170
|
+
| `investigate_failed_logins` | Analyze failed logins and lockout status without changing anything |
|
|
171
|
+
|
|
172
|
+
**Resources** are read-only JSON documents, filtered by the same realm allowlist:
|
|
173
|
+
|
|
174
|
+
| URI | Content |
|
|
175
|
+
| -------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
176
|
+
| `keycloak://realms` | Names of accessible realms |
|
|
177
|
+
| `keycloak://realms/{realm}` | Safe subset of the realm settings |
|
|
178
|
+
| `keycloak://realms/{realm}/security-summary` | Brute-force, password policy, SSL, OTP, event flags, client exposure counts (no secrets) |
|
|
179
|
+
| `keycloak://realms/{realm}/users/{userId}` | One user's basic fields (no attributes, no credentials) |
|
|
180
|
+
|
|
181
|
+
## Security
|
|
182
|
+
|
|
183
|
+
- **Least privilege first.** Use a service-account client scoped to one realm with only the `realm-management` roles you need. A role matrix is in [docs/configuration.md](docs/configuration.md#minimal-roles-per-tool).
|
|
184
|
+
- **Destructive operations need confirmation** (`KEYCLOAK_MCP_REQUIRE_CONFIRMATION=true`), and granting administrative roles does too.
|
|
185
|
+
- **Blast-radius limits:** `KEYCLOAK_MCP_READ_ONLY`, `KEYCLOAK_MCP_REALMS`, and write protection for `master` (off by default).
|
|
186
|
+
- **No secrets in output or logs.** Credentials are masked on stderr; tools never return client secrets, user credentials or attributes.
|
|
187
|
+
- **Prompt injection:** treat names and descriptions coming from Keycloak as data. The shipped prompts say so explicitly; keep write tools disabled when you do not need them.
|
|
188
|
+
|
|
189
|
+
Threat model, hardening checklist and how to report a vulnerability: [docs/security.md](docs/security.md) and [SECURITY.md](SECURITY.md).
|
|
190
|
+
|
|
191
|
+
## Example prompts
|
|
192
|
+
|
|
193
|
+
- "Which realms can you see? Show me the settings of realm `demo`."
|
|
194
|
+
- "Find all users in `demo` whose e-mail ends with `@example.com` and who are disabled."
|
|
195
|
+
- "Create user `jdoe` (Jane Doe, jane@example.com) in `demo`, add her to group `engineering` and send her an e-mail to set a password."
|
|
196
|
+
- "Who is in the group `ops`, and which roles does `alice` have, including composite roles?"
|
|
197
|
+
- "Run the `audit_realm_security` prompt for `demo`." _(needs `clients` and `events` toolsets for the full audit)_
|
|
198
|
+
- "There were many failed logins last night. Investigate realm `demo`."
|
|
199
|
+
- "Offboard `bob` in `demo`: end his sessions and disable the account."
|
|
200
|
+
|
|
201
|
+
## Documentation
|
|
202
|
+
|
|
203
|
+
| Document | Content |
|
|
204
|
+
| -------------------------------------------------- | --------------------------------------------------------------- |
|
|
205
|
+
| [docs/getting-started.md](docs/getting-started.md) | Local Keycloak, service-account setup, first run, MCP Inspector |
|
|
206
|
+
| [docs/configuration.md](docs/configuration.md) | Environment reference, role matrix, role profiles, TLS |
|
|
207
|
+
| [docs/clients.md](docs/clients.md) | Claude Desktop, Claude Code, VS Code, Cursor, generic clients |
|
|
208
|
+
| [docs/tools-reference.md](docs/tools-reference.md) | Every tool in detail |
|
|
209
|
+
| [docs/security.md](docs/security.md) | Threat model and hardening |
|
|
210
|
+
| [docs/troubleshooting.md](docs/troubleshooting.md) | Symptoms, causes, fixes |
|
|
211
|
+
| [docs/architecture.md](docs/architecture.md) | Components, flows, design decisions |
|
|
212
|
+
| [CHANGELOG.md](CHANGELOG.md) | Release notes |
|
|
213
|
+
|
|
214
|
+
## Upgrade from 0.x
|
|
215
|
+
|
|
216
|
+
Version 1.0 changes the defaults: credentials are required (no `admin`/`admin` fallback), destructive tools ask for confirmation, and the tools were renamed to `<domain>_<verb>` (for example `create-user` became `user_create`, `delete-user` became `user_delete`). The old `KEYCLOAK_ADMIN` and `KEYCLOAK_ADMIN_PASSWORD` variables still work but are deprecated. See the [migration guide](docs/migration-1.0.md).
|
|
217
|
+
|
|
218
|
+
## Contributing and license
|
|
219
|
+
|
|
220
|
+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md). Released under the [MIT License](LICENSE).
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { LOG_LEVELS } from "./logging.js";
|
|
3
|
+
export const TOOLSET_NAMES = [
|
|
4
|
+
"core",
|
|
5
|
+
"groups",
|
|
6
|
+
"roles",
|
|
7
|
+
"clients",
|
|
8
|
+
"sessions",
|
|
9
|
+
"events",
|
|
10
|
+
"security",
|
|
11
|
+
];
|
|
12
|
+
export const DEFAULT_TOOLSETS = ["core", "groups", "roles"];
|
|
13
|
+
export class ConfigError extends Error {
|
|
14
|
+
issues;
|
|
15
|
+
constructor(issues) {
|
|
16
|
+
super(`Invalid configuration:\n${issues.map((issue) => ` - ${issue}`).join("\n")}`);
|
|
17
|
+
this.name = "ConfigError";
|
|
18
|
+
this.issues = issues;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const blankToUndefined = (value) => {
|
|
22
|
+
if (typeof value !== "string")
|
|
23
|
+
return value;
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed === "" ? undefined : trimmed;
|
|
26
|
+
};
|
|
27
|
+
const optionalString = z.preprocess(blankToUndefined, z.string().optional());
|
|
28
|
+
const flag = (fallback) => z.preprocess(blankToUndefined, z.stringbool().default(fallback));
|
|
29
|
+
const commaList = z.preprocess(blankToUndefined, z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.transform((value) => (value ?? "")
|
|
33
|
+
.split(",")
|
|
34
|
+
.map((item) => item.trim())
|
|
35
|
+
.filter((item) => item.length > 0)));
|
|
36
|
+
const EnvSchema = z.object({
|
|
37
|
+
KEYCLOAK_URL: z.preprocess(blankToUndefined, z
|
|
38
|
+
.string({ error: "is required (base URL of the Keycloak server)" })
|
|
39
|
+
.pipe(z.url({ protocol: /^https?$/, error: "must be a valid http(s) URL" }))),
|
|
40
|
+
KEYCLOAK_AUTH_REALM: z.preprocess(blankToUndefined, z.string().default("master")),
|
|
41
|
+
KEYCLOAK_CLIENT_ID: optionalString,
|
|
42
|
+
KEYCLOAK_CLIENT_SECRET: optionalString,
|
|
43
|
+
KEYCLOAK_USERNAME: optionalString,
|
|
44
|
+
KEYCLOAK_PASSWORD: optionalString,
|
|
45
|
+
KEYCLOAK_ADMIN: optionalString,
|
|
46
|
+
KEYCLOAK_ADMIN_PASSWORD: optionalString,
|
|
47
|
+
KEYCLOAK_GRANT_TYPE: z.preprocess(blankToUndefined, z
|
|
48
|
+
.enum(["client_credentials", "password"], {
|
|
49
|
+
error: "must be 'client_credentials' or 'password'",
|
|
50
|
+
})
|
|
51
|
+
.optional()),
|
|
52
|
+
KEYCLOAK_MCP_READ_ONLY: flag(false),
|
|
53
|
+
KEYCLOAK_MCP_REALMS: commaList,
|
|
54
|
+
KEYCLOAK_MCP_ALLOW_MASTER_WRITE: flag(false),
|
|
55
|
+
KEYCLOAK_MCP_TOOLSETS: commaList,
|
|
56
|
+
KEYCLOAK_MCP_REQUIRE_CONFIRMATION: flag(true),
|
|
57
|
+
KEYCLOAK_MCP_TIMEOUT_MS: z.preprocess(blankToUndefined, z.coerce
|
|
58
|
+
.number({ error: "must be a number of milliseconds" })
|
|
59
|
+
.int({ error: "must be an integer" })
|
|
60
|
+
.min(1, { error: "must be at least 1" })
|
|
61
|
+
.default(30_000)),
|
|
62
|
+
KEYCLOAK_MCP_LOG_LEVEL: z.preprocess(blankToUndefined, z.enum(LOG_LEVELS, { error: `must be one of: ${LOG_LEVELS.join(", ")}` }).default("info")),
|
|
63
|
+
});
|
|
64
|
+
function resolveToolsets(names, issues) {
|
|
65
|
+
if (names.length === 0)
|
|
66
|
+
return new Set(DEFAULT_TOOLSETS);
|
|
67
|
+
if (names.includes("all"))
|
|
68
|
+
return new Set(TOOLSET_NAMES);
|
|
69
|
+
const result = new Set();
|
|
70
|
+
for (const name of names) {
|
|
71
|
+
if (TOOLSET_NAMES.includes(name)) {
|
|
72
|
+
result.add(name);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
issues.push(`KEYCLOAK_MCP_TOOLSETS: unknown toolset '${name}' (valid: ${TOOLSET_NAMES.join(", ")}, all)`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parses and validates the environment. There are deliberately no credential
|
|
82
|
+
* defaults: missing or contradictory settings fail fast with a ConfigError.
|
|
83
|
+
*/
|
|
84
|
+
export function loadConfig(env = process.env) {
|
|
85
|
+
const parsed = EnvSchema.safeParse(env);
|
|
86
|
+
if (!parsed.success) {
|
|
87
|
+
throw new ConfigError(parsed.error.issues.map((issue) => `${issue.path.join(".") || "env"}: ${issue.message}`));
|
|
88
|
+
}
|
|
89
|
+
const e = parsed.data;
|
|
90
|
+
const issues = [];
|
|
91
|
+
const warnings = [];
|
|
92
|
+
const username = e.KEYCLOAK_USERNAME ?? e.KEYCLOAK_ADMIN;
|
|
93
|
+
const password = e.KEYCLOAK_PASSWORD ?? e.KEYCLOAK_ADMIN_PASSWORD;
|
|
94
|
+
if (e.KEYCLOAK_ADMIN !== undefined && e.KEYCLOAK_USERNAME === undefined) {
|
|
95
|
+
warnings.push("KEYCLOAK_ADMIN is deprecated; use KEYCLOAK_USERNAME instead.");
|
|
96
|
+
}
|
|
97
|
+
if (e.KEYCLOAK_ADMIN_PASSWORD !== undefined && e.KEYCLOAK_PASSWORD === undefined) {
|
|
98
|
+
warnings.push("KEYCLOAK_ADMIN_PASSWORD is deprecated; use KEYCLOAK_PASSWORD instead.");
|
|
99
|
+
}
|
|
100
|
+
const grantType = e.KEYCLOAK_GRANT_TYPE ??
|
|
101
|
+
(e.KEYCLOAK_CLIENT_SECRET !== undefined
|
|
102
|
+
? "client_credentials"
|
|
103
|
+
: username !== undefined
|
|
104
|
+
? "password"
|
|
105
|
+
: undefined);
|
|
106
|
+
let credentials;
|
|
107
|
+
if (grantType === undefined) {
|
|
108
|
+
issues.push("No credentials configured: set KEYCLOAK_CLIENT_ID + KEYCLOAK_CLIENT_SECRET (client_credentials) or KEYCLOAK_USERNAME + KEYCLOAK_PASSWORD (password).");
|
|
109
|
+
}
|
|
110
|
+
else if (grantType === "client_credentials") {
|
|
111
|
+
if (e.KEYCLOAK_CLIENT_ID === undefined) {
|
|
112
|
+
issues.push("KEYCLOAK_CLIENT_ID: is required for the client_credentials grant");
|
|
113
|
+
}
|
|
114
|
+
if (e.KEYCLOAK_CLIENT_SECRET === undefined) {
|
|
115
|
+
issues.push("KEYCLOAK_CLIENT_SECRET: is required for the client_credentials grant");
|
|
116
|
+
}
|
|
117
|
+
if (e.KEYCLOAK_CLIENT_ID !== undefined && e.KEYCLOAK_CLIENT_SECRET !== undefined) {
|
|
118
|
+
credentials = {
|
|
119
|
+
grantType,
|
|
120
|
+
clientId: e.KEYCLOAK_CLIENT_ID,
|
|
121
|
+
clientSecret: e.KEYCLOAK_CLIENT_SECRET,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (username === undefined)
|
|
127
|
+
issues.push("KEYCLOAK_USERNAME: is required for the password grant");
|
|
128
|
+
if (password === undefined)
|
|
129
|
+
issues.push("KEYCLOAK_PASSWORD: is required for the password grant");
|
|
130
|
+
if (username !== undefined && password !== undefined) {
|
|
131
|
+
credentials = {
|
|
132
|
+
grantType,
|
|
133
|
+
clientId: e.KEYCLOAK_CLIENT_ID ?? "admin-cli",
|
|
134
|
+
...(e.KEYCLOAK_CLIENT_SECRET !== undefined && { clientSecret: e.KEYCLOAK_CLIENT_SECRET }),
|
|
135
|
+
username,
|
|
136
|
+
password,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const toolsets = resolveToolsets(e.KEYCLOAK_MCP_TOOLSETS, issues);
|
|
141
|
+
if (issues.length > 0 || credentials === undefined) {
|
|
142
|
+
throw new ConfigError(issues);
|
|
143
|
+
}
|
|
144
|
+
const { protocol, hostname } = new URL(e.KEYCLOAK_URL);
|
|
145
|
+
if (protocol === "http:" && !isLoopbackHost(hostname)) {
|
|
146
|
+
warnings.push(`KEYCLOAK_URL uses plain http:// for the remote host '${hostname}'; tokens and credentials travel unencrypted. Use https://.`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
url: e.KEYCLOAK_URL.replace(/\/+$/, ""),
|
|
150
|
+
authRealm: e.KEYCLOAK_AUTH_REALM,
|
|
151
|
+
credentials,
|
|
152
|
+
readOnly: e.KEYCLOAK_MCP_READ_ONLY,
|
|
153
|
+
allowedRealms: e.KEYCLOAK_MCP_REALMS,
|
|
154
|
+
allowMasterWrite: e.KEYCLOAK_MCP_ALLOW_MASTER_WRITE,
|
|
155
|
+
toolsets,
|
|
156
|
+
requireConfirmation: e.KEYCLOAK_MCP_REQUIRE_CONFIRMATION,
|
|
157
|
+
timeoutMs: e.KEYCLOAK_MCP_TIMEOUT_MS,
|
|
158
|
+
logLevel: e.KEYCLOAK_MCP_LOG_LEVEL,
|
|
159
|
+
warnings,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function isLoopbackHost(hostname) {
|
|
163
|
+
return (hostname === "localhost" ||
|
|
164
|
+
hostname.endsWith(".localhost") ||
|
|
165
|
+
hostname === "[::1]" ||
|
|
166
|
+
hostname.startsWith("127."));
|
|
167
|
+
}
|
|
168
|
+
/** Values that must be masked in logs. */
|
|
169
|
+
export function collectSecrets(config) {
|
|
170
|
+
const { credentials } = config;
|
|
171
|
+
const secrets = [];
|
|
172
|
+
if (credentials.clientSecret)
|
|
173
|
+
secrets.push(credentials.clientSecret);
|
|
174
|
+
if (credentials.grantType === "password")
|
|
175
|
+
secrets.push(credentials.password);
|
|
176
|
+
return secrets;
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/guards.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ToolError } from "./keycloak/errors.js";
|
|
2
|
+
function hasControlChars(value) {
|
|
3
|
+
for (const char of value) {
|
|
4
|
+
const code = char.codePointAt(0) ?? 0;
|
|
5
|
+
if (code < 0x20 || code === 0x7f)
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Validates a value that ends up as a URL path segment (realm name, user id, ...).
|
|
12
|
+
* Rejects separators, dot segments and control characters.
|
|
13
|
+
*/
|
|
14
|
+
export function assertPathSegment(value, label = "value") {
|
|
15
|
+
if (value.length === 0 ||
|
|
16
|
+
value.length > 255 ||
|
|
17
|
+
value === "." ||
|
|
18
|
+
value === ".." ||
|
|
19
|
+
value.includes("/") ||
|
|
20
|
+
value.includes("\\") ||
|
|
21
|
+
hasControlChars(value)) {
|
|
22
|
+
throw new ToolError("guard", `Invalid ${label}: must be non-empty and must not contain '/', '\\', '..' or control characters.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Checks the realm against the allowlist and the master write protection. */
|
|
26
|
+
export function assertRealmAllowed(policy, realm, options) {
|
|
27
|
+
assertPathSegment(realm, "realm");
|
|
28
|
+
if (policy.allowedRealms.length > 0 && !policy.allowedRealms.includes(realm)) {
|
|
29
|
+
throw new ToolError("guard", `Realm '${realm}' is not permitted by KEYCLOAK_MCP_REALMS (allowed: ${policy.allowedRealms.join(", ")}).`);
|
|
30
|
+
}
|
|
31
|
+
if (options.write && realm.toLowerCase() === "master" && !policy.allowMasterWrite) {
|
|
32
|
+
throw new ToolError("guard", "Write operations in the 'master' realm are disabled. Set KEYCLOAK_MCP_ALLOW_MASTER_WRITE=true to allow them.");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function isRealmAllowed(policy, realm) {
|
|
36
|
+
return policy.allowedRealms.length === 0 || policy.allowedRealms.includes(realm);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Destructive tools must be called with confirm=true when confirmation is required.
|
|
40
|
+
* Throws a ToolError carrying the preview so the model can ask the user and retry.
|
|
41
|
+
*/
|
|
42
|
+
export function requireConfirmation(policy, confirm, preview) {
|
|
43
|
+
if (!policy.requireConfirmation || confirm === true)
|
|
44
|
+
return;
|
|
45
|
+
throw new ToolError("confirmation_required", `Confirmation required. This would: ${preview} Nothing was changed. Ask the user for approval, then call this tool again with confirm: true.`);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../src/guards.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAIjD,SAAS,eAAe,CAAC,KAAa;IACpC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;IAChD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAa,EAAE,KAAK,GAAG,OAAO;IAC9D,IACE,KAAK,CAAC,MAAM,KAAK,CAAC;QAClB,KAAK,CAAC,MAAM,GAAG,GAAG;QAClB,KAAK,KAAK,GAAG;QACb,KAAK,KAAK,IAAI;QACd,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QACnB,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QACpB,eAAe,CAAC,KAAK,CAAC,EACtB,CAAC;QACD,MAAM,IAAI,SAAS,CACjB,OAAO,EACP,WAAW,KAAK,iFAAiF,CAClG,CAAC;IACJ,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,kBAAkB,CAChC,MAAmB,EACnB,KAAa,EACb,OAA2B;IAE3B,iBAAiB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7E,MAAM,IAAI,SAAS,CACjB,OAAO,EACP,UAAU,KAAK,uDAAuD,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC1G,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,gBAAgB,EAAE,CAAC;QAClF,MAAM,IAAI,SAAS,CACjB,OAAO,EACP,8GAA8G,CAC/G,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAmB,EAAE,KAAa;IAC/D,OAAO,MAAM,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACnF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAA2C,EAC3C,OAA4B,EAC5B,OAAe;IAEf,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO;IAC5D,MAAM,IAAI,SAAS,CACjB,uBAAuB,EACvB,sCAAsC,OAAO,gGAAgG,CAC9I,CAAC;AACJ,CAAC"}
|