keycloak-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AIKAWA Shigechika
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.
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: keycloak-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for KeyCloak Admin REST API via Service Account
5
+ Author: AIKAWA Shigechika
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/shigechika/keycloak-mcp
8
+ Project-URL: Repository, https://github.com/shigechika/keycloak-mcp
9
+ Project-URL: Issues, https://github.com/shigechika/keycloak-mcp/issues
10
+ Keywords: keycloak,mcp,model-context-protocol,sso,authentication
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: System :: Systems Administration
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: mcp>=1.0
23
+ Requires-Dist: httpx
24
+ Dynamic: license-file
25
+
26
+ <!-- mcp-name: io.github.shigechika/keycloak-mcp -->
27
+
28
+ # keycloak-mcp
29
+
30
+ English | [日本語](README.ja.md)
31
+
32
+ MCP (Model Context Protocol) server for [KeyCloak](https://www.keycloak.org/) Admin REST API.
33
+
34
+ Uses **Client Credentials Grant** (Service Account) — no user password or TOTP required.
35
+ Infinispan-safe: does not create user sessions or use the userinfo endpoint.
36
+
37
+ ## Features
38
+
39
+ ### User Management
40
+
41
+ | Tool | Description |
42
+ |------|-------------|
43
+ | `count_users` | Get total user count in the realm |
44
+ | `search_users` | Search users by username, email, or name |
45
+ | `get_user` | Get detailed user information by username |
46
+ | `reset_password` | Reset a user's password |
47
+ | `reset_passwords_batch` | Reset passwords for multiple users from CSV |
48
+ | `get_user_sessions` | Get active sessions for a user |
49
+
50
+ ### Group Management
51
+
52
+ | Tool | Description |
53
+ |------|-------------|
54
+ | `list_user_groups` | List groups a user belongs to |
55
+ | `list_users_by_group` | List all members of a group |
56
+
57
+ ### Security Monitoring
58
+
59
+ | Tool | Description |
60
+ |------|-------------|
61
+ | `get_brute_force_status` | Check if a user is locked by brute force detection |
62
+ | `get_login_failures_by_ip` | Login failure statistics by source IP |
63
+
64
+ ### Event Analytics
65
+
66
+ | Tool | Description |
67
+ |------|-------------|
68
+ | `get_events` | Get KeyCloak events with filters (type, user, date) |
69
+ | `get_login_stats` | Login success/failure statistics with pagination |
70
+ | `get_login_stats_by_hour` | Login statistics by hour (local time) |
71
+ | `get_login_stats_by_client` | Login statistics by client (SP) |
72
+ | `get_password_update_events` | Password update event history |
73
+
74
+ ### Session & Client
75
+
76
+ | Tool | Description |
77
+ |------|-------------|
78
+ | `get_session_stats` | Active session count per client |
79
+ | `get_client_sessions` | Active sessions for a specific client |
80
+ | `list_clients` | List all SAML/OIDC clients |
81
+ | `get_realm_roles` | List all realm-level roles |
82
+
83
+ ## Setup
84
+
85
+ ```bash
86
+ # uv
87
+ uv pip install keycloak-mcp
88
+
89
+ # pip
90
+ pip install keycloak-mcp
91
+ ```
92
+
93
+ Or from source:
94
+
95
+ ```bash
96
+ git clone https://github.com/shigechika/keycloak-mcp.git
97
+ cd keycloak-mcp
98
+
99
+ # uv
100
+ uv sync
101
+
102
+ # pip
103
+ pip install -e .
104
+ ```
105
+
106
+ ## Configuration
107
+
108
+ Set the following environment variables:
109
+
110
+ | Variable | Description | Default |
111
+ |---|---|---|
112
+ | `KEYCLOAK_URL` | KeyCloak base URL (e.g., `https://sso.example.com`) | *required* |
113
+ | `KEYCLOAK_REALM` | Realm name | `master` |
114
+ | `KEYCLOAK_CLIENT_ID` | Service Account client ID | *required* |
115
+ | `KEYCLOAK_CLIENT_SECRET` | Client secret | *required* |
116
+
117
+ ### KeyCloak Client Setup
118
+
119
+ 1. Create a new client in KeyCloak Admin Console
120
+ 2. Enable **Client authentication** and **Service account roles**
121
+ 3. Assign realm roles: `view-users`, `view-events`, `view-clients`, `manage-users` (for password reset)
122
+
123
+ ## Usage
124
+
125
+ ### Claude Code
126
+
127
+ Add to `.mcp.json`:
128
+
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "keycloak-mcp": {
133
+ "type": "stdio",
134
+ "command": "keycloak-mcp",
135
+ "env": {
136
+ "KEYCLOAK_URL": "https://sso.example.com",
137
+ "KEYCLOAK_CLIENT_ID": "keycloak-mcp",
138
+ "KEYCLOAK_CLIENT_SECRET": ""
139
+ }
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### Claude Desktop
146
+
147
+ Add to `claude_desktop_config.json`:
148
+
149
+ ```json
150
+ {
151
+ "mcpServers": {
152
+ "keycloak-mcp": {
153
+ "command": "keycloak-mcp",
154
+ "env": {
155
+ "KEYCLOAK_URL": "https://sso.example.com",
156
+ "KEYCLOAK_CLIENT_ID": "keycloak-mcp",
157
+ "KEYCLOAK_CLIENT_SECRET": ""
158
+ }
159
+ }
160
+ }
161
+ }
162
+ ```
163
+
164
+ ### Direct Execution
165
+
166
+ ```bash
167
+ export KEYCLOAK_URL=https://sso.example.com
168
+ export KEYCLOAK_CLIENT_ID=keycloak-mcp
169
+ export KEYCLOAK_CLIENT_SECRET=your-secret
170
+ keycloak-mcp
171
+ ```
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ git clone https://github.com/shigechika/keycloak-mcp.git
177
+ cd keycloak-mcp
178
+
179
+ # uv
180
+ uv sync --dev
181
+ uv run pytest -v
182
+ uv run ruff check .
183
+
184
+ # pip
185
+ python3 -m venv .venv
186
+ .venv/bin/pip install -e . && .venv/bin/pip install pytest pytest-cov respx ruff
187
+ .venv/bin/pytest -v
188
+ .venv/bin/ruff check .
189
+ ```
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,168 @@
1
+ <!-- mcp-name: io.github.shigechika/keycloak-mcp -->
2
+
3
+ # keycloak-mcp
4
+
5
+ English | [日本語](README.ja.md)
6
+
7
+ MCP (Model Context Protocol) server for [KeyCloak](https://www.keycloak.org/) Admin REST API.
8
+
9
+ Uses **Client Credentials Grant** (Service Account) — no user password or TOTP required.
10
+ Infinispan-safe: does not create user sessions or use the userinfo endpoint.
11
+
12
+ ## Features
13
+
14
+ ### User Management
15
+
16
+ | Tool | Description |
17
+ |------|-------------|
18
+ | `count_users` | Get total user count in the realm |
19
+ | `search_users` | Search users by username, email, or name |
20
+ | `get_user` | Get detailed user information by username |
21
+ | `reset_password` | Reset a user's password |
22
+ | `reset_passwords_batch` | Reset passwords for multiple users from CSV |
23
+ | `get_user_sessions` | Get active sessions for a user |
24
+
25
+ ### Group Management
26
+
27
+ | Tool | Description |
28
+ |------|-------------|
29
+ | `list_user_groups` | List groups a user belongs to |
30
+ | `list_users_by_group` | List all members of a group |
31
+
32
+ ### Security Monitoring
33
+
34
+ | Tool | Description |
35
+ |------|-------------|
36
+ | `get_brute_force_status` | Check if a user is locked by brute force detection |
37
+ | `get_login_failures_by_ip` | Login failure statistics by source IP |
38
+
39
+ ### Event Analytics
40
+
41
+ | Tool | Description |
42
+ |------|-------------|
43
+ | `get_events` | Get KeyCloak events with filters (type, user, date) |
44
+ | `get_login_stats` | Login success/failure statistics with pagination |
45
+ | `get_login_stats_by_hour` | Login statistics by hour (local time) |
46
+ | `get_login_stats_by_client` | Login statistics by client (SP) |
47
+ | `get_password_update_events` | Password update event history |
48
+
49
+ ### Session & Client
50
+
51
+ | Tool | Description |
52
+ |------|-------------|
53
+ | `get_session_stats` | Active session count per client |
54
+ | `get_client_sessions` | Active sessions for a specific client |
55
+ | `list_clients` | List all SAML/OIDC clients |
56
+ | `get_realm_roles` | List all realm-level roles |
57
+
58
+ ## Setup
59
+
60
+ ```bash
61
+ # uv
62
+ uv pip install keycloak-mcp
63
+
64
+ # pip
65
+ pip install keycloak-mcp
66
+ ```
67
+
68
+ Or from source:
69
+
70
+ ```bash
71
+ git clone https://github.com/shigechika/keycloak-mcp.git
72
+ cd keycloak-mcp
73
+
74
+ # uv
75
+ uv sync
76
+
77
+ # pip
78
+ pip install -e .
79
+ ```
80
+
81
+ ## Configuration
82
+
83
+ Set the following environment variables:
84
+
85
+ | Variable | Description | Default |
86
+ |---|---|---|
87
+ | `KEYCLOAK_URL` | KeyCloak base URL (e.g., `https://sso.example.com`) | *required* |
88
+ | `KEYCLOAK_REALM` | Realm name | `master` |
89
+ | `KEYCLOAK_CLIENT_ID` | Service Account client ID | *required* |
90
+ | `KEYCLOAK_CLIENT_SECRET` | Client secret | *required* |
91
+
92
+ ### KeyCloak Client Setup
93
+
94
+ 1. Create a new client in KeyCloak Admin Console
95
+ 2. Enable **Client authentication** and **Service account roles**
96
+ 3. Assign realm roles: `view-users`, `view-events`, `view-clients`, `manage-users` (for password reset)
97
+
98
+ ## Usage
99
+
100
+ ### Claude Code
101
+
102
+ Add to `.mcp.json`:
103
+
104
+ ```json
105
+ {
106
+ "mcpServers": {
107
+ "keycloak-mcp": {
108
+ "type": "stdio",
109
+ "command": "keycloak-mcp",
110
+ "env": {
111
+ "KEYCLOAK_URL": "https://sso.example.com",
112
+ "KEYCLOAK_CLIENT_ID": "keycloak-mcp",
113
+ "KEYCLOAK_CLIENT_SECRET": ""
114
+ }
115
+ }
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### Claude Desktop
121
+
122
+ Add to `claude_desktop_config.json`:
123
+
124
+ ```json
125
+ {
126
+ "mcpServers": {
127
+ "keycloak-mcp": {
128
+ "command": "keycloak-mcp",
129
+ "env": {
130
+ "KEYCLOAK_URL": "https://sso.example.com",
131
+ "KEYCLOAK_CLIENT_ID": "keycloak-mcp",
132
+ "KEYCLOAK_CLIENT_SECRET": ""
133
+ }
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ### Direct Execution
140
+
141
+ ```bash
142
+ export KEYCLOAK_URL=https://sso.example.com
143
+ export KEYCLOAK_CLIENT_ID=keycloak-mcp
144
+ export KEYCLOAK_CLIENT_SECRET=your-secret
145
+ keycloak-mcp
146
+ ```
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ git clone https://github.com/shigechika/keycloak-mcp.git
152
+ cd keycloak-mcp
153
+
154
+ # uv
155
+ uv sync --dev
156
+ uv run pytest -v
157
+ uv run ruff check .
158
+
159
+ # pip
160
+ python3 -m venv .venv
161
+ .venv/bin/pip install -e . && .venv/bin/pip install pytest pytest-cov respx ruff
162
+ .venv/bin/pytest -v
163
+ .venv/bin/ruff check .
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT
@@ -0,0 +1,3 @@
1
+ """KeyCloak MCP Server — Admin REST API via Service Account."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Entry point for keycloak-mcp MCP server."""
2
+
3
+ from .server import mcp
4
+
5
+ mcp.run(transport="stdio")
@@ -0,0 +1,58 @@
1
+ """Token management for KeyCloak Service Account (Client Credentials Grant)."""
2
+
3
+ import os
4
+ import time
5
+
6
+ import httpx
7
+
8
+
9
+ class TokenManager:
10
+ """Manage KeyCloak access tokens with automatic refresh.
11
+
12
+ Tokens are refreshed 30 seconds before expiry to avoid mid-request failures.
13
+ """
14
+
15
+ def __init__(self):
16
+ self.url = os.environ["KEYCLOAK_URL"].rstrip("/")
17
+ self.realm = os.environ.get("KEYCLOAK_REALM", "master")
18
+ self.client_id = os.environ["KEYCLOAK_CLIENT_ID"]
19
+ self.client_secret = os.environ["KEYCLOAK_CLIENT_SECRET"]
20
+ self._token = None
21
+ self._expires_at = 0
22
+
23
+ @property
24
+ def token_endpoint(self) -> str:
25
+ """Return the OIDC token endpoint URL."""
26
+ return f"{self.url}/realms/{self.realm}/protocol/openid-connect/token"
27
+
28
+ @property
29
+ def admin_base(self) -> str:
30
+ """Return the Admin REST API base URL."""
31
+ return f"{self.url}/admin/realms/{self.realm}"
32
+
33
+ def get_token(self) -> str:
34
+ """Return a valid access token, refreshing if needed."""
35
+ if self._token and time.time() < self._expires_at - 30:
36
+ return self._token
37
+ return self._refresh()
38
+
39
+ def _refresh(self) -> str:
40
+ """Fetch a new token via Client Credentials Grant."""
41
+ resp = httpx.post(
42
+ self.token_endpoint,
43
+ data={
44
+ "grant_type": "client_credentials",
45
+ "client_id": self.client_id,
46
+ "client_secret": self.client_secret,
47
+ },
48
+ timeout=10,
49
+ )
50
+ resp.raise_for_status()
51
+ data = resp.json()
52
+ self._token = data["access_token"]
53
+ self._expires_at = time.time() + data.get("expires_in", 300)
54
+ return self._token
55
+
56
+ def headers(self) -> dict:
57
+ """Return Authorization headers with a valid Bearer token."""
58
+ return {"Authorization": f"Bearer {self.get_token()}"}
@@ -0,0 +1,163 @@
1
+ """KeyCloak Admin REST API client."""
2
+
3
+ from typing import Any
4
+
5
+ import httpx
6
+
7
+ from .auth import TokenManager
8
+
9
+
10
+ class KeyCloakClient:
11
+ """Thin wrapper around the KeyCloak Admin REST API."""
12
+
13
+ def __init__(self):
14
+ self.auth = TokenManager()
15
+ self._http = httpx.Client(timeout=30)
16
+
17
+ def _get(self, path: str, params: dict | None = None) -> Any:
18
+ """GET request to Admin API."""
19
+ url = f"{self.auth.admin_base}{path}"
20
+ resp = self._http.get(url, headers=self.auth.headers(), params=params or {})
21
+ resp.raise_for_status()
22
+ return resp.json()
23
+
24
+ def _put(self, path: str, json: dict | None = None) -> int:
25
+ """PUT request to Admin API. Returns status code."""
26
+ url = f"{self.auth.admin_base}{path}"
27
+ resp = self._http.put(url, headers=self.auth.headers(), json=json or {})
28
+ resp.raise_for_status()
29
+ return resp.status_code
30
+
31
+ # --- Users ---
32
+
33
+ def count_users(self) -> int:
34
+ """Return total user count."""
35
+ return self._get("/users/count")
36
+
37
+ def search_users(self, query: str, max_results: int = 20) -> list[dict]:
38
+ """Search users by username, email, or name."""
39
+ return self._get("/users", {"search": query, "max": max_results})
40
+
41
+ def get_user(self, user_id: str) -> dict:
42
+ """Get user by ID."""
43
+ return self._get(f"/users/{user_id}")
44
+
45
+ def get_user_by_username(self, username: str) -> dict | None:
46
+ """Get user by exact username. Returns None if not found."""
47
+ users = self._get("/users", {"username": username, "exact": "true"})
48
+ return users[0] if users else None
49
+
50
+ def reset_password(self, user_id: str, password: str, temporary: bool = False) -> int:
51
+ """Reset a user's password."""
52
+ return self._put(
53
+ f"/users/{user_id}/reset-password",
54
+ {"type": "password", "value": password, "temporary": temporary},
55
+ )
56
+
57
+ def get_user_sessions(self, user_id: str) -> list[dict]:
58
+ """Get active sessions for a user."""
59
+ return self._get(f"/users/{user_id}/sessions")
60
+
61
+ def get_user_roles(self, user_id: str) -> dict:
62
+ """Get role mappings for a user."""
63
+ return self._get(f"/users/{user_id}/role-mappings")
64
+
65
+ def get_user_groups(self, user_id: str) -> list[dict]:
66
+ """Get groups a user belongs to."""
67
+ return self._get(f"/users/{user_id}/groups")
68
+
69
+ # --- Brute Force ---
70
+
71
+ def get_brute_force_status(self, user_id: str) -> dict:
72
+ """Get brute force detection status for a user."""
73
+ return self._get(f"/attack-detection/brute-force/users/{user_id}")
74
+
75
+ # --- Groups ---
76
+
77
+ def list_groups(self, max_results: int = 100) -> list[dict]:
78
+ """List all groups."""
79
+ return self._get("/groups", {"max": max_results})
80
+
81
+ def get_group_members(self, group_id: str, max_results: int = 100) -> list[dict]:
82
+ """Get members of a group."""
83
+ return self._get(f"/groups/{group_id}/members", {"max": max_results})
84
+
85
+ # --- Events ---
86
+
87
+ def get_events(
88
+ self,
89
+ event_type: str | None = None,
90
+ user: str | None = None,
91
+ date_from: str | None = None,
92
+ date_to: str | None = None,
93
+ max_results: int = 100,
94
+ ) -> list[dict]:
95
+ """Get events with optional filters (single page)."""
96
+ params: dict[str, Any] = {"max": max_results}
97
+ if event_type:
98
+ params["type"] = event_type
99
+ if user:
100
+ params["user"] = user
101
+ if date_from:
102
+ params["dateFrom"] = date_from
103
+ if date_to:
104
+ params["dateTo"] = date_to
105
+ return self._get("/events", params)
106
+
107
+ def get_events_all(
108
+ self,
109
+ event_type: str | None = None,
110
+ user: str | None = None,
111
+ date_from: str | None = None,
112
+ date_to: str | None = None,
113
+ page_size: int = 1000,
114
+ ) -> list[dict]:
115
+ """Get all events with automatic pagination."""
116
+ params: dict[str, Any] = {"max": page_size, "first": 0}
117
+ if event_type:
118
+ params["type"] = event_type
119
+ if user:
120
+ params["user"] = user
121
+ if date_from:
122
+ params["dateFrom"] = date_from
123
+ if date_to:
124
+ params["dateTo"] = date_to
125
+ all_events: list[dict] = []
126
+ while True:
127
+ page = self._get("/events", params)
128
+ all_events.extend(page)
129
+ if len(page) < page_size:
130
+ break
131
+ params["first"] += page_size
132
+ return all_events
133
+
134
+ # --- Sessions ---
135
+
136
+ def get_session_stats(self) -> list[dict]:
137
+ """Get client session statistics."""
138
+ return self._get("/client-session-stats")
139
+
140
+ # --- Clients ---
141
+
142
+ def list_clients(self, max_results: int = 100) -> list[dict]:
143
+ """List all clients."""
144
+ return self._get("/clients", {"max": max_results})
145
+
146
+ def get_client(self, client_id: str) -> dict:
147
+ """Get client by internal ID."""
148
+ return self._get(f"/clients/{client_id}")
149
+
150
+ def get_client_by_client_id(self, client_id: str) -> dict | None:
151
+ """Get client by clientId (not internal UUID)."""
152
+ clients = self._get("/clients", {"clientId": client_id})
153
+ return clients[0] if clients else None
154
+
155
+ def get_client_sessions(self, internal_id: str, max_results: int = 100) -> list[dict]:
156
+ """Get active sessions for a client."""
157
+ return self._get(f"/clients/{internal_id}/user-sessions", {"max": max_results})
158
+
159
+ # --- Roles ---
160
+
161
+ def get_realm_roles(self) -> list[dict]:
162
+ """List realm roles."""
163
+ return self._get("/roles")