keycloak-mcp
Version:
MCP server for Keycloak
381 lines (380 loc) • 12.7 kB
JavaScript
import KcAdminClient from "@keycloak/keycloak-admin-client";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const server = new Server({
name: "keycloak-admin",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
const kcAdminClient = new KcAdminClient({
baseUrl: process.env.KEYCLOAK_URL || "http://localhost:8080",
realmName: "master",
});
const CreateUserSchema = z.object({
realm: z.string(),
username: z.string(),
email: z.string().email(),
firstName: z.string(),
lastName: z.string(),
});
const DeleteUserSchema = z.object({
realm: z.string(),
userId: z.string(),
});
const ListUsersSchema = z.object({
realm: z.string(),
});
const AssignClientRoleSchema = z.object({
realm: z.string(),
userId: z.string(),
clientUniqueId: z.string(),
roleName: z.string(),
});
const AddUserToGroupSchema = z.object({
realm: z.string(),
userId: z.string(),
groupId: z.string(),
});
const ListClientsSchema = z.object({
realm: z.string(),
});
const ListGroupsSchema = z.object({
realm: z.string(),
});
const ListClientRolesSchema = z.object({
realm: z.string(),
clientUniqueId: z.string(),
});
async function authenticate() {
await kcAdminClient.auth({
username: process.env.KEYCLOAK_ADMIN || "admin",
password: process.env.KEYCLOAK_ADMIN_PASSWORD || "admin",
grantType: "password",
clientId: "admin-cli",
});
}
// input schema for the tools
const inputSchema = {
"create-user": {
type: "object",
properties: {
realm: { type: "string" },
username: { type: "string" },
email: { type: "string", format: "email" },
firstName: { type: "string" },
lastName: { type: "string" },
},
required: ["realm", "username", "email", "firstName", "lastName"],
},
"delete-user": {
type: "object",
properties: {
realm: { type: "string" },
userId: { type: "string" },
},
required: ["realm", "userId"],
},
"list-realms": {
type: "object",
properties: {},
required: [],
},
"list-users": {
type: "object",
properties: {
realm: { type: "string" },
},
required: ["realm"],
},
"assign-client-role-to-user": {
type: "object",
properties: {
realm: { type: "string" },
userId: { type: "string" },
clientUniqueId: { type: "string" },
roleName: { type: "string" },
},
required: ["realm", "userId", "clientUniqueId", "roleName"],
},
"add-user-to-group": {
type: "object",
properties: {
realm: { type: "string" },
userId: { type: "string" },
groupId: { type: "string" },
},
required: ["realm", "userId", "groupId"],
},
"list-clients": {
type: "object",
properties: {
realm: { type: "string" },
},
required: ["realm"],
},
"list-groups": {
type: "object",
properties: {
realm: { type: "string" },
},
required: ["realm"],
},
"list-client-roles": {
type: "object",
properties: {
realm: z.string(),
clientUniqueId: z.string(),
},
required: ["realm", "clientUniqueId"],
},
};
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "create-user",
description: "Create a new user in a specific realm",
inputSchema: inputSchema["create-user"],
},
{
name: "delete-user",
description: "Delete a user from a specific realm",
inputSchema: inputSchema["delete-user"],
},
{
name: "list-realms",
description: "List all available realms",
inputSchema: inputSchema["list-realms"],
},
{
name: "list-users",
description: "List users in a specific realm",
inputSchema: inputSchema["list-users"],
},
{
name: "assign-client-role-to-user",
description: "Assign a client role to a user",
inputSchema: inputSchema["assign-client-role-to-user"],
},
{
name: "add-user-to-group",
description: "Add a user to a group",
inputSchema: inputSchema["add-user-to-group"],
},
{
name: "list-clients",
description: "List clients in a specific realm",
inputSchema: inputSchema["list-clients"],
},
{
name: "list-groups",
description: "List groups in a specific realm",
inputSchema: inputSchema["list-groups"],
},
{
name: "list-client-roles",
description: "List roles in a specific client",
inputSchema: inputSchema["list-client-roles"],
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
await authenticate();
const name = request.params.name;
const args = request.params.arguments ?? {};
try {
switch (name) {
case "create-user": {
const { realm, username, email, firstName, lastName } = CreateUserSchema.parse(args);
const user = await kcAdminClient.users.create({
realm,
username,
email,
firstName,
lastName,
enabled: true,
});
return {
content: [
{
type: "text",
text: `User created successfully. User ID: ${user.id}`,
},
],
};
}
case "delete-user": {
const { realm, userId } = DeleteUserSchema.parse(args);
await kcAdminClient.users.del({ id: userId, realm });
return {
content: [
{
type: "text",
text: `User ${userId} deleted successfully from realm ${realm}`,
},
],
};
}
case "list-realms": {
const realms = await kcAdminClient.realms.find();
return {
content: [
{
type: "text",
text: `Available realms:\n${realms
.map((r) => `- ${r.realm} (${r.id})`)
.join("\n")}`,
},
],
};
}
case "list-users": {
const { realm } = ListUsersSchema.parse(args);
const users = await kcAdminClient.users.find({
realm,
});
return {
content: [
{
type: "text",
text: `Users in realm ${realm}:\n${users
.map((u) => `- ${u.username} (${u.id})`)
.join("\n")}`,
},
],
};
}
case "assign-client-role-to-user": {
const { realm, userId, clientUniqueId, roleName } = AssignClientRoleSchema.parse(args);
const roles = await kcAdminClient.clients.listRoles({
id: clientUniqueId,
realm,
});
const role = roles.find((r) => r.name === roleName);
if (!role || !role.id || !role.name) {
return {
isError: true,
content: [
{
type: "text",
text: `Role '${roleName}' not found or has no ID.`,
},
],
};
}
await kcAdminClient.users.addClientRoleMappings({
realm,
id: userId,
clientUniqueId,
roles: [
{
id: role.id, // safely asserted since we check below
name: role.name,
},
],
});
return {
content: [
{
type: "text",
text: `Assigned role '${roleName}' to user ${userId} in client ${clientUniqueId}`,
},
],
};
}
case "add-user-to-group": {
const { realm, userId, groupId } = AddUserToGroupSchema.parse(args);
await kcAdminClient.users.addToGroup({
realm,
id: userId,
groupId,
});
return {
content: [
{
type: "text",
text: `User ${userId} added to group ${groupId} in realm ${realm}`,
},
],
};
}
case "list-clients": {
const { realm } = ListClientsSchema.parse(args);
const clients = await kcAdminClient.clients.find({ realm });
return {
content: [
{
type: "text",
text: `Clients in realm ${realm}:\n${clients
.map((c) => `- ${c.clientId} (${c.id})`)
.join("\n")}`,
},
],
};
}
case "list-groups": {
const { realm } = ListGroupsSchema.parse(args);
const groups = await kcAdminClient.groups.find({ realm });
return {
content: [
{
type: "text",
text: `Groups in realm ${realm}:\n${groups
.map((g) => `- ${g.name} (${g.id})`)
.join("\n")}`,
},
],
};
}
case "list-client-roles": {
const { realm, clientUniqueId } = ListClientRolesSchema.parse(args);
const roles = await kcAdminClient.clients.listRoles({
id: clientUniqueId,
realm,
});
return {
content: [
{
type: "text",
text: `Roles in client ${clientUniqueId} in realm ${realm}:\n${roles
.map((r) => `- ${r.name}`)
.join("\n")}`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
if (error instanceof z.ZodError) {
return {
isError: true,
content: [
{
type: "text",
text: `Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`,
},
],
};
}
throw error;
}
});
const transport = new StdioServerTransport();
try {
await server.connect(transport);
console.log(`Keycloak MCP Server running`);
}
catch (error) {
console.error("Failed to connect server:", error);
}