mcp-keycloak-admin 0.1.0 → 0.2.1
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/README.md +74 -3
- package/dist/index.js +422 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# mcp-keycloak-admin
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/mcp-keycloak-admin)
|
|
4
|
+
[](https://github.com/mrz1880/mcp-keycloak-admin/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/mrz1880/mcp-keycloak-admin/actions/workflows/codeql.yml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
3
8
|
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server to
|
|
4
9
|
administer a [Keycloak](https://www.keycloak.org) instance through its Admin
|
|
5
10
|
REST API. Safe by default, configurable, and built with a clean, test-driven
|
|
@@ -7,9 +12,18 @@ architecture.
|
|
|
7
12
|
|
|
8
13
|
Compatible with **Keycloak 26.x** (validated against 26.0.5).
|
|
9
14
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
No install needed — run it straight from npm with `npx`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx -y mcp-keycloak-admin
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The server speaks MCP over stdio, so you normally wire it into an MCP client
|
|
24
|
+
rather than running it by hand — see [Usage with an MCP client](#usage-with-an-mcp-client).
|
|
25
|
+
New to it? The [Quickstart](docs/quickstart.md) spins up a local Keycloak and a
|
|
26
|
+
client config in a couple of minutes.
|
|
13
27
|
|
|
14
28
|
## Why
|
|
15
29
|
|
|
@@ -64,6 +78,49 @@ Add the server to your MCP client configuration:
|
|
|
64
78
|
See [docs/setup-keycloak.md](docs/setup-keycloak.md) to create the `mcp-admin`
|
|
65
79
|
client and grant it the least-privilege roles it needs.
|
|
66
80
|
|
|
81
|
+
### Multiple Keycloak instances
|
|
82
|
+
|
|
83
|
+
Each server entry targets **one** Keycloak (one base URL + realm + auth). To
|
|
84
|
+
manage several environments, add **one entry per instance** — each fully
|
|
85
|
+
isolated, with its own credentials and guardrails:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"kc-preprod": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["-y", "mcp-keycloak-admin"],
|
|
93
|
+
"env": {
|
|
94
|
+
"KEYCLOAK_BASE_URL": "https://preprod.example.com",
|
|
95
|
+
"KEYCLOAK_REALM": "Pandi-Panda-Preprod",
|
|
96
|
+
"AUTH_MODE": "service_account",
|
|
97
|
+
"KC_CLIENT_ID": "mcp-admin",
|
|
98
|
+
"KC_CLIENT_SECRET": "…",
|
|
99
|
+
"ALLOWED_REALMS": "Pandi-Panda-Preprod"
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"kc-prod": {
|
|
103
|
+
"command": "npx",
|
|
104
|
+
"args": ["-y", "mcp-keycloak-admin"],
|
|
105
|
+
"env": {
|
|
106
|
+
"KEYCLOAK_BASE_URL": "https://auth.example.com",
|
|
107
|
+
"KEYCLOAK_REALM": "Pandi-Panda",
|
|
108
|
+
"AUTH_MODE": "service_account",
|
|
109
|
+
"KC_CLIENT_ID": "mcp-admin",
|
|
110
|
+
"KC_CLIENT_SECRET": "…",
|
|
111
|
+
"READ_ONLY": "true",
|
|
112
|
+
"ALLOWED_REALMS": "Pandi-Panda"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The client namespaces the tools per server (e.g. `kc-prod:keycloak_user_delete`),
|
|
120
|
+
so there's no risk of running an operation against the wrong environment. This
|
|
121
|
+
is the recommended pattern: you can, for example, keep production `READ_ONLY`
|
|
122
|
+
while preprod stays writable.
|
|
123
|
+
|
|
67
124
|
## Configuration
|
|
68
125
|
|
|
69
126
|
| Variable | Required | Description |
|
|
@@ -89,6 +146,11 @@ confirmation). Every tool carries the matching MCP annotations
|
|
|
89
146
|
|
|
90
147
|
Currently implemented:
|
|
91
148
|
|
|
149
|
+
> **Note:** this table tracks the `main` branch, which can run ahead of the
|
|
150
|
+
> latest npm release shown by the version badge above. To confirm what's
|
|
151
|
+
> available in your install, check `npm view mcp-keycloak-admin version` and pin
|
|
152
|
+
> `mcp-keycloak-admin@latest`.
|
|
153
|
+
|
|
92
154
|
| Tool | Level | Description |
|
|
93
155
|
| ------------------------------------------- | ----- | ----------------------------------------------------- |
|
|
94
156
|
| `keycloak_user_search` | R | Search realm users by email, username or free text. |
|
|
@@ -105,7 +167,14 @@ Currently implemented:
|
|
|
105
167
|
| `keycloak_user_roles_get` | R | List a user's realm roles. |
|
|
106
168
|
| `keycloak_user_role_assign` | W | Grant a realm role to a user. |
|
|
107
169
|
| `keycloak_user_role_unassign` | D | Revoke a realm role from a user. |
|
|
170
|
+
| `keycloak_client_roles_list` | R | List the roles defined on a client. |
|
|
171
|
+
| `keycloak_user_client_roles_get` | R | List a user's client roles. |
|
|
172
|
+
| `keycloak_user_client_role_assign` | W | Grant a client role to a user. |
|
|
173
|
+
| `keycloak_user_client_role_unassign` | D | Revoke a client role from a user. |
|
|
108
174
|
| `keycloak_client_list` | R | List the realm clients. |
|
|
175
|
+
| `keycloak_client_create` | W | Create a realm client. |
|
|
176
|
+
| `keycloak_client_update` | W | Update a client (enabled, public, redirect URIs). |
|
|
177
|
+
| `keycloak_client_delete` | D | Delete a client. |
|
|
109
178
|
| `keycloak_client_get` | R | Fetch a client by its clientId. |
|
|
110
179
|
| `keycloak_client_get_secret` | R | Read a client secret (masked unless `reveal`). |
|
|
111
180
|
| `keycloak_client_scopes_list` | R | List the realm's client scopes. |
|
|
@@ -164,6 +233,8 @@ npm run check # typecheck + lint + format check + unit tests
|
|
|
164
233
|
npm run build # bundle to dist/
|
|
165
234
|
```
|
|
166
235
|
|
|
236
|
+
Releases are automated — see [docs/releasing.md](docs/releasing.md).
|
|
237
|
+
|
|
167
238
|
## Contributing
|
|
168
239
|
|
|
169
240
|
Contributions are welcome — please read [CONTRIBUTING.md](CONTRIBUTING.md).
|
package/dist/index.js
CHANGED
|
@@ -451,7 +451,7 @@ var KeycloakClientRepository = class {
|
|
|
451
451
|
}
|
|
452
452
|
client;
|
|
453
453
|
async list() {
|
|
454
|
-
const raw = await this.client.
|
|
454
|
+
const raw = await this.client.list("/clients");
|
|
455
455
|
return raw.map(toSummary);
|
|
456
456
|
}
|
|
457
457
|
async findByClientId(clientId) {
|
|
@@ -473,6 +473,30 @@ var KeycloakClientRepository = class {
|
|
|
473
473
|
);
|
|
474
474
|
return ClientSecret.fromString(raw.value);
|
|
475
475
|
}
|
|
476
|
+
create(client) {
|
|
477
|
+
return this.client.post("/clients", {
|
|
478
|
+
clientId: client.clientId.toString(),
|
|
479
|
+
enabled: client.enabled,
|
|
480
|
+
publicClient: client.publicClient,
|
|
481
|
+
redirectUris: client.redirectUris
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
update(uuid, changes) {
|
|
485
|
+
const body = {};
|
|
486
|
+
if (changes.enabled !== void 0) {
|
|
487
|
+
body.enabled = changes.enabled;
|
|
488
|
+
}
|
|
489
|
+
if (changes.publicClient !== void 0) {
|
|
490
|
+
body.publicClient = changes.publicClient;
|
|
491
|
+
}
|
|
492
|
+
if (changes.redirectUris !== void 0) {
|
|
493
|
+
body.redirectUris = changes.redirectUris;
|
|
494
|
+
}
|
|
495
|
+
return this.client.put(`/clients/${uuid.toString()}`, body);
|
|
496
|
+
}
|
|
497
|
+
delete(uuid) {
|
|
498
|
+
return this.client.delete(`/clients/${uuid.toString()}`);
|
|
499
|
+
}
|
|
476
500
|
};
|
|
477
501
|
|
|
478
502
|
// src/infrastructure/keycloak/authentication-repository.ts
|
|
@@ -963,7 +987,7 @@ var KeycloakGroupRepository = class {
|
|
|
963
987
|
}
|
|
964
988
|
client;
|
|
965
989
|
async list() {
|
|
966
|
-
const raw = await this.client.
|
|
990
|
+
const raw = await this.client.list("/groups");
|
|
967
991
|
return raw.map(toGroup);
|
|
968
992
|
}
|
|
969
993
|
create(name) {
|
|
@@ -1121,7 +1145,7 @@ var KeycloakRoleRepository = class {
|
|
|
1121
1145
|
}
|
|
1122
1146
|
client;
|
|
1123
1147
|
async listRealmRoles() {
|
|
1124
|
-
const raw = await this.client.
|
|
1148
|
+
const raw = await this.client.list("/roles");
|
|
1125
1149
|
return raw.map(toRole);
|
|
1126
1150
|
}
|
|
1127
1151
|
async findRealmRole(name) {
|
|
@@ -1154,6 +1178,43 @@ var KeycloakRoleRepository = class {
|
|
|
1154
1178
|
[toRepresentation(role)]
|
|
1155
1179
|
);
|
|
1156
1180
|
}
|
|
1181
|
+
async listClientRoles(clientUuid) {
|
|
1182
|
+
const raw = await this.client.list(
|
|
1183
|
+
`/clients/${clientUuid.toString()}/roles`
|
|
1184
|
+
);
|
|
1185
|
+
return raw.map(toRole);
|
|
1186
|
+
}
|
|
1187
|
+
async findClientRole(clientUuid, name) {
|
|
1188
|
+
try {
|
|
1189
|
+
const raw = await this.client.getJson(
|
|
1190
|
+
`/clients/${clientUuid.toString()}/roles/${encodeURIComponent(name.toString())}`
|
|
1191
|
+
);
|
|
1192
|
+
return toRole(raw);
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (error instanceof KeycloakError && error.status === 404) {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
throw error;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
async listUserClientRoles(userId, clientUuid) {
|
|
1201
|
+
const raw = await this.client.getJson(
|
|
1202
|
+
`/users/${userId.toString()}/role-mappings/clients/${clientUuid.toString()}`
|
|
1203
|
+
);
|
|
1204
|
+
return raw.map(toRole);
|
|
1205
|
+
}
|
|
1206
|
+
assignClientRole(userId, clientUuid, role) {
|
|
1207
|
+
return this.client.post(
|
|
1208
|
+
`/users/${userId.toString()}/role-mappings/clients/${clientUuid.toString()}`,
|
|
1209
|
+
[toRepresentation(role)]
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
removeClientRole(userId, clientUuid, role) {
|
|
1213
|
+
return this.client.delete(
|
|
1214
|
+
`/users/${userId.toString()}/role-mappings/clients/${clientUuid.toString()}`,
|
|
1215
|
+
[toRepresentation(role)]
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1157
1218
|
};
|
|
1158
1219
|
|
|
1159
1220
|
// src/infrastructure/mcp/auth-tools.ts
|
|
@@ -1646,6 +1707,42 @@ function buildClientScopeTools(deps) {
|
|
|
1646
1707
|
// src/infrastructure/mcp/client-tools.ts
|
|
1647
1708
|
import { z as z5 } from "zod";
|
|
1648
1709
|
|
|
1710
|
+
// src/application/clients/create-client.use-case.ts
|
|
1711
|
+
var CreateClientUseCase = class {
|
|
1712
|
+
constructor(clients) {
|
|
1713
|
+
this.clients = clients;
|
|
1714
|
+
}
|
|
1715
|
+
clients;
|
|
1716
|
+
execute(client) {
|
|
1717
|
+
return this.clients.create(client);
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// src/application/clients/delete-client.use-case.ts
|
|
1722
|
+
var DeleteClientUseCase = class {
|
|
1723
|
+
constructor(clients, confirmer) {
|
|
1724
|
+
this.clients = clients;
|
|
1725
|
+
this.confirmer = confirmer;
|
|
1726
|
+
}
|
|
1727
|
+
clients;
|
|
1728
|
+
confirmer;
|
|
1729
|
+
async execute(clientId) {
|
|
1730
|
+
const client = await this.clients.findByClientId(clientId);
|
|
1731
|
+
if (client === null) {
|
|
1732
|
+
return { deleted: false, reason: "Client not found" };
|
|
1733
|
+
}
|
|
1734
|
+
const operation = DestructiveOperation.of(
|
|
1735
|
+
`Delete client ${clientId.toString()}`,
|
|
1736
|
+
"Removes the client and everything attached to it (roles, scopes, service account). Applications using it stop working. Irreversible."
|
|
1737
|
+
);
|
|
1738
|
+
if (!await this.confirmer.confirm(operation)) {
|
|
1739
|
+
return { deleted: false, reason: "Operation not confirmed" };
|
|
1740
|
+
}
|
|
1741
|
+
await this.clients.delete(client.uuid);
|
|
1742
|
+
return { deleted: true };
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
|
|
1649
1746
|
// src/application/clients/get-client-secret.use-case.ts
|
|
1650
1747
|
var GetClientSecretUseCase = class {
|
|
1651
1748
|
constructor(clients) {
|
|
@@ -1708,6 +1805,22 @@ var RegenerateClientSecretUseCase = class {
|
|
|
1708
1805
|
}
|
|
1709
1806
|
};
|
|
1710
1807
|
|
|
1808
|
+
// src/application/clients/update-client.use-case.ts
|
|
1809
|
+
var UpdateClientUseCase = class {
|
|
1810
|
+
constructor(clients) {
|
|
1811
|
+
this.clients = clients;
|
|
1812
|
+
}
|
|
1813
|
+
clients;
|
|
1814
|
+
async execute(input) {
|
|
1815
|
+
const client = await this.clients.findByClientId(input.clientId);
|
|
1816
|
+
if (client === null) {
|
|
1817
|
+
return { updated: false, reason: "Client not found" };
|
|
1818
|
+
}
|
|
1819
|
+
await this.clients.update(client.uuid, input.changes);
|
|
1820
|
+
return { updated: true };
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1711
1824
|
// src/infrastructure/mcp/client-tools.ts
|
|
1712
1825
|
function serializeClient(client) {
|
|
1713
1826
|
return {
|
|
@@ -1811,12 +1924,110 @@ function regenerateClientSecretTool(deps) {
|
|
|
1811
1924
|
}
|
|
1812
1925
|
};
|
|
1813
1926
|
}
|
|
1927
|
+
function readStringArray(value) {
|
|
1928
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1929
|
+
}
|
|
1930
|
+
function createClientTool(deps) {
|
|
1931
|
+
return {
|
|
1932
|
+
name: "keycloak_client_create",
|
|
1933
|
+
title: "Create client",
|
|
1934
|
+
description: "Create a realm client.",
|
|
1935
|
+
level: "write" /* Write */,
|
|
1936
|
+
inputSchema: {
|
|
1937
|
+
clientId: z5.string(),
|
|
1938
|
+
enabled: z5.boolean().optional(),
|
|
1939
|
+
publicClient: z5.boolean().optional(),
|
|
1940
|
+
redirectUris: z5.array(z5.string()).optional()
|
|
1941
|
+
},
|
|
1942
|
+
annotations: {
|
|
1943
|
+
readOnlyHint: false,
|
|
1944
|
+
destructiveHint: false,
|
|
1945
|
+
idempotentHint: false
|
|
1946
|
+
},
|
|
1947
|
+
async handler(args) {
|
|
1948
|
+
await new CreateClientUseCase(deps.clientRepository).execute({
|
|
1949
|
+
clientId: ClientId.fromString(String(args.clientId)),
|
|
1950
|
+
enabled: args.enabled !== false,
|
|
1951
|
+
publicClient: args.publicClient === true,
|
|
1952
|
+
redirectUris: readStringArray(args.redirectUris)
|
|
1953
|
+
});
|
|
1954
|
+
return textResult(`Client "${String(args.clientId)}" created.`);
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function updateClientTool(deps) {
|
|
1959
|
+
return {
|
|
1960
|
+
name: "keycloak_client_update",
|
|
1961
|
+
title: "Update client",
|
|
1962
|
+
description: "Update a client's enabled, public flag or redirect URIs.",
|
|
1963
|
+
level: "write" /* Write */,
|
|
1964
|
+
inputSchema: {
|
|
1965
|
+
clientId: z5.string(),
|
|
1966
|
+
enabled: z5.boolean().optional(),
|
|
1967
|
+
publicClient: z5.boolean().optional(),
|
|
1968
|
+
redirectUris: z5.array(z5.string()).optional()
|
|
1969
|
+
},
|
|
1970
|
+
annotations: {
|
|
1971
|
+
readOnlyHint: false,
|
|
1972
|
+
destructiveHint: false,
|
|
1973
|
+
idempotentHint: true
|
|
1974
|
+
},
|
|
1975
|
+
async handler(args) {
|
|
1976
|
+
const changes = {};
|
|
1977
|
+
if (typeof args.enabled === "boolean") {
|
|
1978
|
+
changes.enabled = args.enabled;
|
|
1979
|
+
}
|
|
1980
|
+
if (typeof args.publicClient === "boolean") {
|
|
1981
|
+
changes.publicClient = args.publicClient;
|
|
1982
|
+
}
|
|
1983
|
+
if (Array.isArray(args.redirectUris)) {
|
|
1984
|
+
changes.redirectUris = readStringArray(args.redirectUris);
|
|
1985
|
+
}
|
|
1986
|
+
const result = await new UpdateClientUseCase(
|
|
1987
|
+
deps.clientRepository
|
|
1988
|
+
).execute({
|
|
1989
|
+
clientId: ClientId.fromString(String(args.clientId)),
|
|
1990
|
+
changes
|
|
1991
|
+
});
|
|
1992
|
+
return textResult(
|
|
1993
|
+
result.updated ? `Client "${String(args.clientId)}" updated.` : `Not updated: ${result.reason ?? "unknown reason"}`
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function deleteClientTool(deps) {
|
|
1999
|
+
return {
|
|
2000
|
+
name: "keycloak_client_delete",
|
|
2001
|
+
title: "Delete client",
|
|
2002
|
+
description: "Delete a client. Requires confirmation.",
|
|
2003
|
+
level: "destructive" /* Destructive */,
|
|
2004
|
+
inputSchema: { clientId: z5.string(), confirm: z5.boolean().optional() },
|
|
2005
|
+
annotations: {
|
|
2006
|
+
readOnlyHint: false,
|
|
2007
|
+
destructiveHint: true,
|
|
2008
|
+
idempotentHint: false
|
|
2009
|
+
},
|
|
2010
|
+
async handler(args) {
|
|
2011
|
+
const confirmer = deps.confirmers.create(args.confirm === true);
|
|
2012
|
+
const result = await new DeleteClientUseCase(
|
|
2013
|
+
deps.clientRepository,
|
|
2014
|
+
confirmer
|
|
2015
|
+
).execute(ClientId.fromString(String(args.clientId)));
|
|
2016
|
+
return textResult(
|
|
2017
|
+
result.deleted ? `Client "${String(args.clientId)}" deleted.` : `Not deleted: ${result.reason ?? "unknown reason"}`
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
}
|
|
1814
2022
|
function buildClientTools(deps) {
|
|
1815
2023
|
return [
|
|
1816
2024
|
listClientsTool(deps),
|
|
1817
2025
|
getClientTool(deps),
|
|
1818
2026
|
getClientSecretTool(deps),
|
|
1819
|
-
|
|
2027
|
+
createClientTool(deps),
|
|
2028
|
+
updateClientTool(deps),
|
|
2029
|
+
regenerateClientSecretTool(deps),
|
|
2030
|
+
deleteClientTool(deps)
|
|
1820
2031
|
];
|
|
1821
2032
|
}
|
|
1822
2033
|
|
|
@@ -2708,6 +2919,28 @@ function buildIdpTools(deps) {
|
|
|
2708
2919
|
// src/infrastructure/mcp/role-tools.ts
|
|
2709
2920
|
import { z as z10 } from "zod";
|
|
2710
2921
|
|
|
2922
|
+
// src/application/roles/assign-client-role.use-case.ts
|
|
2923
|
+
var AssignClientRoleUseCase = class {
|
|
2924
|
+
constructor(clients, roles) {
|
|
2925
|
+
this.clients = clients;
|
|
2926
|
+
this.roles = roles;
|
|
2927
|
+
}
|
|
2928
|
+
clients;
|
|
2929
|
+
roles;
|
|
2930
|
+
async execute(input) {
|
|
2931
|
+
const client = await this.clients.findByClientId(input.clientId);
|
|
2932
|
+
if (client === null) {
|
|
2933
|
+
return { assigned: false, reason: "Client not found" };
|
|
2934
|
+
}
|
|
2935
|
+
const role = await this.roles.findClientRole(client.uuid, input.role);
|
|
2936
|
+
if (role === null) {
|
|
2937
|
+
return { assigned: false, reason: "Role not found" };
|
|
2938
|
+
}
|
|
2939
|
+
await this.roles.assignClientRole(input.userId, client.uuid, role);
|
|
2940
|
+
return { assigned: true };
|
|
2941
|
+
}
|
|
2942
|
+
};
|
|
2943
|
+
|
|
2711
2944
|
// src/application/roles/assign-user-role.use-case.ts
|
|
2712
2945
|
var AssignUserRoleUseCase = class {
|
|
2713
2946
|
constructor(roles) {
|
|
@@ -2724,6 +2957,23 @@ var AssignUserRoleUseCase = class {
|
|
|
2724
2957
|
}
|
|
2725
2958
|
};
|
|
2726
2959
|
|
|
2960
|
+
// src/application/roles/get-user-client-roles.use-case.ts
|
|
2961
|
+
var GetUserClientRolesUseCase = class {
|
|
2962
|
+
constructor(clients, roles) {
|
|
2963
|
+
this.clients = clients;
|
|
2964
|
+
this.roles = roles;
|
|
2965
|
+
}
|
|
2966
|
+
clients;
|
|
2967
|
+
roles;
|
|
2968
|
+
async execute(input) {
|
|
2969
|
+
const client = await this.clients.findByClientId(input.clientId);
|
|
2970
|
+
if (client === null) {
|
|
2971
|
+
return null;
|
|
2972
|
+
}
|
|
2973
|
+
return this.roles.listUserClientRoles(input.userId, client.uuid);
|
|
2974
|
+
}
|
|
2975
|
+
};
|
|
2976
|
+
|
|
2727
2977
|
// src/application/roles/get-user-roles.use-case.ts
|
|
2728
2978
|
var GetUserRolesUseCase = class {
|
|
2729
2979
|
constructor(roles) {
|
|
@@ -2735,6 +2985,23 @@ var GetUserRolesUseCase = class {
|
|
|
2735
2985
|
}
|
|
2736
2986
|
};
|
|
2737
2987
|
|
|
2988
|
+
// src/application/roles/list-client-roles.use-case.ts
|
|
2989
|
+
var ListClientRolesUseCase = class {
|
|
2990
|
+
constructor(clients, roles) {
|
|
2991
|
+
this.clients = clients;
|
|
2992
|
+
this.roles = roles;
|
|
2993
|
+
}
|
|
2994
|
+
clients;
|
|
2995
|
+
roles;
|
|
2996
|
+
async execute(clientId) {
|
|
2997
|
+
const client = await this.clients.findByClientId(clientId);
|
|
2998
|
+
if (client === null) {
|
|
2999
|
+
return null;
|
|
3000
|
+
}
|
|
3001
|
+
return this.roles.listClientRoles(client.uuid);
|
|
3002
|
+
}
|
|
3003
|
+
};
|
|
3004
|
+
|
|
2738
3005
|
// src/application/roles/list-realm-roles.use-case.ts
|
|
2739
3006
|
var ListRealmRolesUseCase = class {
|
|
2740
3007
|
constructor(roles) {
|
|
@@ -2746,6 +3013,37 @@ var ListRealmRolesUseCase = class {
|
|
|
2746
3013
|
}
|
|
2747
3014
|
};
|
|
2748
3015
|
|
|
3016
|
+
// src/application/roles/unassign-client-role.use-case.ts
|
|
3017
|
+
var UnassignClientRoleUseCase = class {
|
|
3018
|
+
constructor(clients, roles, confirmer) {
|
|
3019
|
+
this.clients = clients;
|
|
3020
|
+
this.roles = roles;
|
|
3021
|
+
this.confirmer = confirmer;
|
|
3022
|
+
}
|
|
3023
|
+
clients;
|
|
3024
|
+
roles;
|
|
3025
|
+
confirmer;
|
|
3026
|
+
async execute(input) {
|
|
3027
|
+
const client = await this.clients.findByClientId(input.clientId);
|
|
3028
|
+
if (client === null) {
|
|
3029
|
+
return { removed: false, reason: "Client not found" };
|
|
3030
|
+
}
|
|
3031
|
+
const role = await this.roles.findClientRole(client.uuid, input.role);
|
|
3032
|
+
if (role === null) {
|
|
3033
|
+
return { removed: false, reason: "Role not found" };
|
|
3034
|
+
}
|
|
3035
|
+
const operation = DestructiveOperation.of(
|
|
3036
|
+
`Remove client role ${input.role.toString()} (client ${input.clientId.toString()}) from user ${input.userId.toString()}`,
|
|
3037
|
+
"The user immediately loses the permissions granted by this client role."
|
|
3038
|
+
);
|
|
3039
|
+
if (!await this.confirmer.confirm(operation)) {
|
|
3040
|
+
return { removed: false, reason: "Operation not confirmed" };
|
|
3041
|
+
}
|
|
3042
|
+
await this.roles.removeClientRole(input.userId, client.uuid, role);
|
|
3043
|
+
return { removed: true };
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
|
|
2749
3047
|
// src/application/roles/unassign-user-role.use-case.ts
|
|
2750
3048
|
var UnassignUserRoleUseCase = class {
|
|
2751
3049
|
constructor(roles, confirmer) {
|
|
@@ -2875,12 +3173,130 @@ function unassignUserRoleTool(deps) {
|
|
|
2875
3173
|
}
|
|
2876
3174
|
};
|
|
2877
3175
|
}
|
|
3176
|
+
function listClientRolesTool(deps) {
|
|
3177
|
+
return {
|
|
3178
|
+
name: "keycloak_client_roles_list",
|
|
3179
|
+
title: "List client roles",
|
|
3180
|
+
description: "List the roles defined on a client.",
|
|
3181
|
+
level: "read" /* Read */,
|
|
3182
|
+
inputSchema: { clientId: z10.string() },
|
|
3183
|
+
annotations: {
|
|
3184
|
+
readOnlyHint: true,
|
|
3185
|
+
destructiveHint: false,
|
|
3186
|
+
idempotentHint: true
|
|
3187
|
+
},
|
|
3188
|
+
async handler(args) {
|
|
3189
|
+
const roles = await new ListClientRolesUseCase(
|
|
3190
|
+
deps.clientRepository,
|
|
3191
|
+
deps.roleRepository
|
|
3192
|
+
).execute(ClientId.fromString(String(args.clientId)));
|
|
3193
|
+
return textResult(
|
|
3194
|
+
roles === null ? "Client not found." : JSON.stringify(roles.map(serializeRole), null, 2)
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
function getUserClientRolesTool(deps) {
|
|
3200
|
+
return {
|
|
3201
|
+
name: "keycloak_user_client_roles_get",
|
|
3202
|
+
title: "Get a user's client roles",
|
|
3203
|
+
description: "List the client roles assigned to a user for a client.",
|
|
3204
|
+
level: "read" /* Read */,
|
|
3205
|
+
inputSchema: { userId: z10.string(), clientId: z10.string() },
|
|
3206
|
+
annotations: {
|
|
3207
|
+
readOnlyHint: true,
|
|
3208
|
+
destructiveHint: false,
|
|
3209
|
+
idempotentHint: true
|
|
3210
|
+
},
|
|
3211
|
+
async handler(args) {
|
|
3212
|
+
const roles = await new GetUserClientRolesUseCase(
|
|
3213
|
+
deps.clientRepository,
|
|
3214
|
+
deps.roleRepository
|
|
3215
|
+
).execute({
|
|
3216
|
+
userId: UserId.fromString(String(args.userId)),
|
|
3217
|
+
clientId: ClientId.fromString(String(args.clientId))
|
|
3218
|
+
});
|
|
3219
|
+
return textResult(
|
|
3220
|
+
roles === null ? "Client not found." : JSON.stringify(roles.map(serializeRole), null, 2)
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
3223
|
+
};
|
|
3224
|
+
}
|
|
3225
|
+
function assignClientRoleTool(deps) {
|
|
3226
|
+
return {
|
|
3227
|
+
name: "keycloak_user_client_role_assign",
|
|
3228
|
+
title: "Assign a client role to a user",
|
|
3229
|
+
description: "Grant a client role to a user.",
|
|
3230
|
+
level: "write" /* Write */,
|
|
3231
|
+
inputSchema: {
|
|
3232
|
+
userId: z10.string(),
|
|
3233
|
+
clientId: z10.string(),
|
|
3234
|
+
role: z10.string()
|
|
3235
|
+
},
|
|
3236
|
+
annotations: {
|
|
3237
|
+
readOnlyHint: false,
|
|
3238
|
+
destructiveHint: false,
|
|
3239
|
+
idempotentHint: true
|
|
3240
|
+
},
|
|
3241
|
+
async handler(args) {
|
|
3242
|
+
const result = await new AssignClientRoleUseCase(
|
|
3243
|
+
deps.clientRepository,
|
|
3244
|
+
deps.roleRepository
|
|
3245
|
+
).execute({
|
|
3246
|
+
userId: UserId.fromString(String(args.userId)),
|
|
3247
|
+
clientId: ClientId.fromString(String(args.clientId)),
|
|
3248
|
+
role: RoleName.fromString(String(args.role))
|
|
3249
|
+
});
|
|
3250
|
+
return textResult(
|
|
3251
|
+
result.assigned ? `Client role "${String(args.role)}" assigned.` : `Not assigned: ${result.reason ?? "unknown reason"}`
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
function unassignClientRoleTool(deps) {
|
|
3257
|
+
return {
|
|
3258
|
+
name: "keycloak_user_client_role_unassign",
|
|
3259
|
+
title: "Remove a client role from a user",
|
|
3260
|
+
description: "Revoke a client role from a user. Requires confirmation.",
|
|
3261
|
+
level: "destructive" /* Destructive */,
|
|
3262
|
+
inputSchema: {
|
|
3263
|
+
userId: z10.string(),
|
|
3264
|
+
clientId: z10.string(),
|
|
3265
|
+
role: z10.string(),
|
|
3266
|
+
confirm: z10.boolean().optional()
|
|
3267
|
+
},
|
|
3268
|
+
annotations: {
|
|
3269
|
+
readOnlyHint: false,
|
|
3270
|
+
destructiveHint: true,
|
|
3271
|
+
idempotentHint: false
|
|
3272
|
+
},
|
|
3273
|
+
async handler(args) {
|
|
3274
|
+
const confirmer = deps.confirmers.create(args.confirm === true);
|
|
3275
|
+
const result = await new UnassignClientRoleUseCase(
|
|
3276
|
+
deps.clientRepository,
|
|
3277
|
+
deps.roleRepository,
|
|
3278
|
+
confirmer
|
|
3279
|
+
).execute({
|
|
3280
|
+
userId: UserId.fromString(String(args.userId)),
|
|
3281
|
+
clientId: ClientId.fromString(String(args.clientId)),
|
|
3282
|
+
role: RoleName.fromString(String(args.role))
|
|
3283
|
+
});
|
|
3284
|
+
return textResult(
|
|
3285
|
+
result.removed ? `Client role "${String(args.role)}" removed.` : `Not removed: ${result.reason ?? "unknown reason"}`
|
|
3286
|
+
);
|
|
3287
|
+
}
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
2878
3290
|
function buildRoleTools(deps) {
|
|
2879
3291
|
return [
|
|
2880
3292
|
listRealmRolesTool(deps),
|
|
2881
3293
|
getUserRolesTool(deps),
|
|
3294
|
+
listClientRolesTool(deps),
|
|
3295
|
+
getUserClientRolesTool(deps),
|
|
2882
3296
|
assignUserRoleTool(deps),
|
|
2883
|
-
|
|
3297
|
+
assignClientRoleTool(deps),
|
|
3298
|
+
unassignUserRoleTool(deps),
|
|
3299
|
+
unassignClientRoleTool(deps)
|
|
2884
3300
|
];
|
|
2885
3301
|
}
|
|
2886
3302
|
|
|
@@ -3486,7 +3902,7 @@ function createServer(config) {
|
|
|
3486
3902
|
const tools = filterTools(
|
|
3487
3903
|
[
|
|
3488
3904
|
...buildUserTools({ userRepository, confirmers }),
|
|
3489
|
-
...buildRoleTools({ roleRepository, confirmers }),
|
|
3905
|
+
...buildRoleTools({ roleRepository, clientRepository, confirmers }),
|
|
3490
3906
|
...buildClientTools({ clientRepository, confirmers }),
|
|
3491
3907
|
...buildClientScopeTools({
|
|
3492
3908
|
clientRepository,
|