// keycloak's tools — moved here from the shared sdk (novox/hq ADR 0039), importing keycloak's own // client. They return structured data (not the hal MCP `{content:[...]}` shape); the mesh serves // them through the sdk's tool harness. Write actions announce themselves through the module's event // surface at the point they succeed. import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { KeycloakClient } from "../client.js"; import { events } from "../index.js"; export function getKeycloakTools(kc: KeycloakClient): ToolDefinition[] { // Almost every tool is realm-scoped; an omitted realm falls back to the one the module resolved // from its environment, so the common single-realm case needs no argument. const realmOf = (args: Readonly>): string => args.realm ? String(args.realm) : kc.defaultRealm; return [ // Realms & sessions { name: "keycloak_list_realms", description: "List all Keycloak realms.", input: {}, run: async () => { const realms = await kc.listRealms(); return { realms: realms.map((r) => ({ id: r.id, realm: r.realm, displayName: r.displayName, enabled: r.enabled })) }; }, }, { name: "keycloak_list_sessions", description: "List active sessions for a user in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, }, run: async (args) => ({ sessions: await kc.getUserSessions(realmOf(args), String(args.user_id)) }), }, // Users { name: "keycloak_list_users", description: "List users in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, search: { type: "string", description: "search by username, email, first/last name" }, max: { type: "number", description: "maximum number of results" }, }, run: async (args) => ({ users: await kc.listUsers(realmOf(args), { search: args.search ? String(args.search) : undefined, max: args.max ? Number(args.max) : undefined, }), }), }, { name: "keycloak_create_user", description: "Create a user in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, username: { type: "string", description: "username" }, email: { type: "string", description: "email address" }, password: { type: "string", description: "initial password" }, temporary_password: { type: "boolean", description: "require a password change on first login (default true)" }, }, run: async (args) => { const realm = realmOf(args); const username = String(args.username); const email = args.email ? String(args.email) : undefined; const credentials = args.password ? [{ type: "password", value: String(args.password), temporary: args.temporary_password !== false }] : undefined; await kc.createUser(realm, { username, email, credentials }); await events.userCreated(realm, username, email); return { created: { realm, username, email } }; }, }, { name: "keycloak_delete_user", description: "Delete a user from a Keycloak realm (requires confirm).", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, confirm: { type: "boolean", description: "must be true to confirm deletion" }, }, run: async (args) => { const realm = realmOf(args); const userId = String(args.user_id); if (args.confirm !== true) return { aborted: "confirm must be true to delete a user" }; await kc.deleteUser(realm, userId); await events.userDeleted(realm, userId); return { deleted: { realm, userId } }; }, }, { name: "keycloak_update_user", description: "Update a user's attributes in a Keycloak realm (enable/disable, change email, name).", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, enabled: { type: "boolean", description: "enable or disable the user" }, email: { type: "string", description: "new email address" }, firstName: { type: "string", description: "new first name" }, lastName: { type: "string", description: "new last name" }, }, run: async (args) => { const realm = realmOf(args); const userId = String(args.user_id); const updates: Record = {}; if (args.enabled !== undefined) updates.enabled = args.enabled === true; if (args.email !== undefined) updates.email = String(args.email); if (args.firstName !== undefined) updates.firstName = String(args.firstName); if (args.lastName !== undefined) updates.lastName = String(args.lastName); if (Object.keys(updates).length === 0) return { aborted: "no updates provided" }; await kc.updateUser(realm, userId, updates); return { updated: { realm, userId, fields: Object.keys(updates) } }; }, }, { name: "keycloak_reset_password", description: "Reset a user's password in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, password: { type: "string", description: "new password" }, temporary: { type: "boolean", description: "require a password change on next login (default false)" }, }, run: async (args) => { const realm = realmOf(args); const userId = String(args.user_id); await kc.resetPassword(realm, userId, String(args.password), args.temporary === true); await events.passwordReset(realm, userId); return { reset: { realm, userId } }; }, }, // Clients { name: "keycloak_list_clients", description: "List OIDC clients in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" } }, run: async (args) => { const clients = (await kc.listClients(realmOf(args))) as Array>; return { clients: clients.map((c) => ({ id: c.id, clientId: c.clientId, name: c.name, enabled: c.enabled, protocol: c.protocol, publicClient: c.publicClient, rootUrl: c.rootUrl, })), }; }, }, { name: "keycloak_create_client", description: "Create an OIDC client in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, client_id: { type: "string", description: "client ID (e.g. 'my-app')" }, name: { type: "string", description: "display name" }, root_url: { type: "string", description: "root URL of the application" }, redirect_uris: { type: "array", description: "allowed redirect URIs" }, public_client: { type: "boolean", description: "public client, no client secret (default true)" }, }, run: async (args) => { const realm = realmOf(args); const clientId = String(args.client_id); const name = args.name ? String(args.name) : undefined; await kc.createClient(realm, { clientId, name, rootUrl: args.root_url ? String(args.root_url) : undefined, redirectUris: Array.isArray(args.redirect_uris) ? args.redirect_uris.map(String) : undefined, publicClient: args.public_client !== false, }); await events.clientCreated(realm, clientId, name); return { created: { realm, clientId, name } }; }, }, { name: "keycloak_delete_client", description: "Delete an OIDC client from a Keycloak realm (requires confirm).", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, client_id: { type: "string", description: "client ID (e.g. 'my-app')" }, confirm: { type: "boolean", description: "must be true to confirm deletion" }, }, run: async (args) => { const realm = realmOf(args); const clientId = String(args.client_id); if (args.confirm !== true) return { aborted: "confirm must be true to delete a client" }; await kc.deleteClient(realm, clientId); return { deleted: { realm, clientId } }; }, }, { name: "keycloak_get_client_secret", description: "Get the client secret for a confidential OIDC client.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, client_id: { type: "string", description: "client ID" }, }, run: async (args) => ({ secret: await kc.getClientSecret(realmOf(args), String(args.client_id)) }), }, { name: "keycloak_add_protocol_mapper", description: "Add a protocol mapper to an OIDC client. Common types: oidc-usermodel-realm-role-mapper " + "(realm roles), oidc-usermodel-attribute-mapper (user attributes), oidc-audience-mapper.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, client_id: { type: "string", description: "client ID (e.g. 'grafana')" }, name: { type: "string", description: "mapper name (e.g. 'realm roles')" }, mapper_type: { type: "string", description: "protocol mapper type (e.g. 'oidc-usermodel-realm-role-mapper')" }, claim_name: { type: "string", description: "token claim name (e.g. 'realm_access.roles')" }, claim_type: { type: "string", description: "JSON type: String, long, int, boolean (default String)" }, multivalued: { type: "boolean", description: "whether the claim has multiple values (default false)" }, id_token: { type: "boolean", description: "include in ID token (default true)" }, access_token: { type: "boolean", description: "include in access token (default true)" }, userinfo: { type: "boolean", description: "include in userinfo response (default true)" }, }, run: async (args) => { const realm = realmOf(args); const clientId = String(args.client_id); const name = String(args.name); await kc.addProtocolMapper(realm, clientId, { name, protocolMapper: String(args.mapper_type), config: { "claim.name": String(args.claim_name), "jsonType.label": args.claim_type ? String(args.claim_type) : "String", "multivalued": String(args.multivalued === true), "id.token.claim": String(args.id_token !== false), "access.token.claim": String(args.access_token !== false), "userinfo.token.claim": String(args.userinfo !== false), }, }); return { added: { realm, clientId, mapper: name } }; }, }, // Groups { name: "keycloak_list_groups", description: "List groups in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" } }, run: async (args) => ({ groups: await kc.listGroups(realmOf(args)) }), }, { name: "keycloak_create_group", description: "Create a group in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, name: { type: "string", description: "group name" }, }, run: async (args) => { const realm = realmOf(args); const name = String(args.name); await kc.createGroup(realm, name); await events.groupCreated(realm, name); return { created: { realm, group: name } }; }, }, { name: "keycloak_get_user_groups", description: "List the groups a user belongs to in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, }, run: async (args) => ({ groups: await kc.getUserGroups(realmOf(args), String(args.user_id)) }), }, { name: "keycloak_add_user_to_group", description: "Add a user to a group in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, group_id: { type: "string", description: "group ID (UUID)" }, }, run: async (args) => { const realm = realmOf(args); await kc.addUserToGroup(realm, String(args.user_id), String(args.group_id)); return { added: { realm, userId: String(args.user_id), groupId: String(args.group_id) } }; }, }, { name: "keycloak_remove_user_from_group", description: "Remove a user from a group in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, group_id: { type: "string", description: "group ID (UUID)" }, }, run: async (args) => { const realm = realmOf(args); await kc.removeUserFromGroup(realm, String(args.user_id), String(args.group_id)); return { removed: { realm, userId: String(args.user_id), groupId: String(args.group_id) } }; }, }, // Roles { name: "keycloak_get_user_roles", description: "List the realm roles assigned to a user in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, }, run: async (args) => ({ roles: await kc.getUserRealmRoles(realmOf(args), String(args.user_id)) }), }, { name: "keycloak_create_role", description: "Create a realm role in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, role_name: { type: "string", description: "role name" }, description: { type: "string", description: "role description" }, }, run: async (args) => { const realm = realmOf(args); const name = String(args.role_name); await kc.createRealmRole(realm, { name, description: args.description ? String(args.description) : undefined }); await events.roleCreated(realm, name); return { created: { realm, role: name } }; }, }, { name: "keycloak_assign_user_role", description: "Assign an existing realm role to a user. Create it first with keycloak_create_role if needed.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, role_name: { type: "string", description: "role name to assign" }, }, run: async (args) => { const realm = realmOf(args); const userId = String(args.user_id); const roleName = String(args.role_name); // The mapping API needs the role's UUID, which only the "available" list carries; if the // role is neither available nor already assigned it does not exist in this realm. const available = await kc.getAvailableRealmRoles(realm, userId); const role = available.find((r) => r.name === roleName); if (!role) { const assigned = await kc.getUserRealmRoles(realm, userId); if (assigned.find((r) => r.name === roleName)) return { alreadyAssigned: { realm, userId, role: roleName } }; return { notFound: { realm, role: roleName } }; } await kc.assignRealmRoles(realm, userId, [{ id: role.id, name: role.name }]); return { assigned: { realm, userId, role: roleName } }; }, }, { name: "keycloak_remove_user_role", description: "Remove a realm role from a user in a Keycloak realm.", input: { realm: { type: "string", description: "realm name (defaults to the module's realm)" }, user_id: { type: "string", description: "user ID (UUID)" }, role_name: { type: "string", description: "role name to remove" }, }, run: async (args) => { const realm = realmOf(args); const userId = String(args.user_id); const roleName = String(args.role_name); const assigned = await kc.getUserRealmRoles(realm, userId); const role = assigned.find((r) => r.name === roleName); if (!role) return { notAssigned: { realm, userId, role: roleName } }; await kc.removeRealmRoles(realm, userId, [{ id: role.id, name: role.name }]); return { removed: { realm, userId, role: roleName } }; }, }, ]; } // The tools exist only when the client can be configured; without an admin password, keycloak // contributes none rather than failing the whole runtime. registerModuleTools("keycloak", (env) => { try { return getKeycloakTools(KeycloakClient.fromEnv(env)); } catch { return []; } });