DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

SBOMs are essential to circumventing software supply chain attacks, and they provide visibility into various software components.

Related

  • The Twelve-Factor Agents: Building Production-Ready LLM Applications
  • My Dive into Local LLMs, Part 2: Taming Personal Finance with Homegrown AI (and Why Privacy Matters)
  • Decoding the Secret Language of LLM Tokenizers
  • Master AI Development: The Ultimate Guide to LangChain, LangGraph, LangFlow, and LangSmith

Trending

  • Jakarta EE 11 and the Road Ahead With Jakarta EE 12
  • From Drift to Discipline: Operating Model for Regaining Enterprise Cloud Control
  • Modernizing Apache Spark Applications With GenAI: Migrating From Java to Scala
  • How Developers Are Driving Supply Chain Innovation With Modern Tech
  1. DZone
  2. Data Engineering
  3. AI/ML
  4. A Keycloak Example: Building My First MCP Server Tools With Quarkus

A Keycloak Example: Building My First MCP Server Tools With Quarkus

Learn to build an MCP server for Keycloak. The article shows how to create a Model Context Protocol (MCP) server for Keycloak using Quarkus and Goose CLI.

By 
Shaaf Syed user avatar
Shaaf Syed
·
Jul. 01, 25 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
2.5K Views

Join the DZone community and get the full member experience.

Join For Free

Recently, I explored how the Model Context Protocol (MCP) is gaining traction in the Java ecosystem, with frameworks like Spring AI, Quarkus, and LangChain4j starting to adopt it for integrating language models via standardized interfaces. It was also time to start experimenting with writing an MCP Server myself (well maybe not the first time). Certainly, I don’t want to be left out of all the cool things being demonstrated by the community. The goal for me is to learn, and creating perhaps a more practical example. 

In this post I am going to choose Keycloak, and write an experimental MCP server implementation for Keycloak. The post is also to spark interest around this topic: Will it be useful to have an MCP server for Keycloak?

What Is Model Context Protocol? 

It is a standard introduced by Anthropic in November 2024. The intention of MCP is to have a standard that helps the community write and consume Tools, Prompts and Resources. Imagine you start writing a tool for Slack and I also start writing a tool for Slack. Lo and behold, we both have our own implementations, but then Slack comes out with its own tools as well. Right now, we’ve got two problems: there’s no standard way to talk to these tools, and honestly, if Slack or GitHub just handled creating and exposing tools for their own services, it’d make life way easier for both of us. This is precisely the kind of scenario where MCP proves to be valuable.


User, LLM, Client, and MCPServer flow


Explanation

  • User sends a query/question to the LLM.
  • LLM analyzes the question and decides if a Tool needs to be invoked.
  • LLM then instructs the client to execute tools.
  • The client executes the Tools on the MCP server.
  • Client then returns the results back to the LLM.
  • And the LLM formulates the results for the User.

While that was just a base example. The reality is that MCP also supports Prompts and Resources. Its also important to state that MCP in generic terms does not really bring new features, but focuses on a Standard. And since its advent, we have multiple MCP servers and implementations in frameworks at our disposal, like Quarkus, Spring AI, MCP SDK, etc.

MCP enables us to develop agents and complex workflows with LLMs by providing e.g. a selection of pre-built integrations and flexibility to switch between LLMs.

Stdio vs SSE

A crucial distinction should be made between local development using Standard IO (where server and client are on the same machine) or building CLI apps versus remote development using Server-Sent Events (SSE) over HTTP (allowing servers to be deployed elsewhere and accessed via API). The latter should be highlighted as the most practical for real-world, multi-application use though. Another important thing to note is that the MCP Server communicates via the JSON-RPC

JSON-RPC is a stateless, light-weight remote procedure call (RPC) protocol. Primarily this specification defines several data structures and the rules around their processing. It is transport agnostic in that the concepts can be used within the same process, over sockets, over http, or in many various message passing environments

Logical breakdown of Java App, MCP Client, and MCP Server

Keycloak

If you aren't familiar with Keycloak, it's an open source identity and access management software. Current version is 26, its used alot already in the wild. It provides single sign on capability with OAuth/OIDC, AD, LDAP and SAML v2 as well. 

Lets get started. 

Create a project from the quarkus cli or via code.quarkus.io.

In my example I use the stdio. This means a CLI based standard-input-output extension.

Add the stdio Quarkus extension in the pom.xml:

        <dependency>
            <groupId>io.quarkiverse.mcp</groupId>
            <artifactId>quarkus-mcp-server-stdio</artifactId>
            <version>1.0.0.Alpha5</version>
        </dependency>


Great. One interesting fact is that Keycloak is also built using Quarkus. Initially it was based on Wildfly, however about two years ago, the team moved the entire thing to Quarkus. Keycloak Admin CLI, a great tool for administration of Keycloak as the name suggests, uses REST API. I will use that for this project. Lets add that to pom.xml as well.

        <dependency>
            <groupId>io.quarkus</groupId>
            <artifactId>quarkus-keycloak-admin-rest-client</artifactId>
        </dependency>


Okay, so that should setup the basics.

Since I am starting this from scratch, time to write the first service. I am going to write a UserService , which will call CRUD operations via the Keycloak Admin client for Users in a realm.

What Is a Realm

A realm is the logical namespace for all configurations,  and options, for a given group or applications or services. A realm secures and manages security metadata for a set of users, applications, and registered identity brokers, clients etc. Users can be created within a specific realm within the Administration console. Roles (permission types) can be defined at the realm level and you can also set up user role mappings to assign these permissions to specific users. A user belongs to and logs into a realm. Realms are isolated from one another and can only manage and authenticate the users that they control.

An image that shows what a realm is.


UserService for Keycloak

Lets start with writing a service class to access Keycloak.

@ApplicationScoped // [1]
public class UserService {
    
    @Inject 
    Keycloak keycloak; // [2]

}


[1] The service starts when the application starts @ApplicationScoped. 

[2] I am also injecting the import org.keycloak.admin.client.Keycloak client to call the admin API on Keycloak.

The following getUsers method takes a realm as input. This means I am forcing the user to specify a realm as there can be multiple realms in a Keycloak installation. Once the realm parameter is received, I can then call the user().list() to get all the users in the realm.

    public List<UserRepresentation> getUsers(String realm) {
        return keycloak.realm(realm).users().list();

    }


For the addUser method, I want User creation parameters and the realm. This way when the Tool call is made, I want to make sure that the context is the realm. For example a user might ask to get Users from two realms and then add user to one of them.

    public String addUser(String realm, String username, String firstName, String lastName, String email, String password) {
        UserRepresentation user = new UserRepresentation(); // setting up User fields [1]
        user.setFirstName(firstName);
        user.setLastName(lastName);
        user.setUsername(username);
        user.setEnabled(true);
        user.setEmail(email);

        CredentialRepresentation credential = new CredentialRepresentation(); // Add the password [2]
        credential.setType(CredentialRepresentation.PASSWORD);
        credential.setValue(password);
        credential.setTemporary(false);
        user.setCredentials(List.of(credential));
        
        Response response = keycloak.realm(realm).users().create(user); // Send creation request [3]
        if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
            return "Successfully created user: " + username;
        } else {
            Log.error("Failed to create user. Status: " + response.getStatus());
            response.close();
            return "Error creating user: "+" "+username;
        }
    }


[1] The UserRepresentation class is part of the Admin REST API and is used to represent a user in the context of managing users within a Keycloak realm. This class encapsulates various attributes and properties associated with a user, allowing administrators to create, update, and retrieve user information programmatically.

[2] The CredentialRepresentation class is used to represent the credentials associated with a user account. This class is part of the Keycloak Admin REST API and is essential for managing user authentication methods, such as passwords, OTP (One-Time Password), and other credential types.

[3] Finally, I send the create user request to Keycloak. There is some more error handling underneath this section to ensure that when a Tool is called, I can return the right status.

Similarly I also implement the following two functions, to delete a user and get a user by its name:

public String deleteUser(String realm, String username)
public UserRepresentation getUserByUsername(String realm, String username)


The full code listing of the UserService is available here on github.

Two more operational things I will need to do, in order for the above to run successfully.

Add the following line to the properties file. This means I am running our local Keycloak instance on port 8081.

quarkus.keycloak.admin-client.server-url=http://localhost:8081


Keycloak Dev-Mode via Docker-Compose

There is also a docker-compose.yaml file for Keycloak that will spin it up locally. All it does is start Keycloak in dev mode, and expose port 8081. Currently, I am running this file with Podman.

services:
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    container_name: keycloak
    environment:
      - KEYCLOAK_ADMIN=admin
      - KEYCLOAK_ADMIN_PASSWORD=admin
    ports:
      - "8081:8080"
    command: >
      start-dev      
    volumes:
      - keycloak_data:/opt/keycloak/data
    restart: unless-stopped

volumes:
  keycloak_data:


To run the above file: docker-compose up.

Creating the UserTool

A Tool is a component that enhances the capabilities of LLMs by enabling them to perform specific actions and interact with external systems. This could be APIs, databases, internal systems etc. Thats exactly what I will do here, build Tools for Keycloak. We create the following flow, where UserTool will call the UserService that in turn calls Keycloak for the operations. I have already created UserService. Now it's time to create UserTool.

An image of a Keycloak flow


In the following UserTool class, I inject UserService and also the ObjectMapper. I am using com.fasterxml.jackson.databind.ObjectMapper to transform some of the results to String, e.g. List.

public class UserTool {

    @Inject
    UserService userService;

    @Inject
    ObjectMapper mapper;
}


Next up, lets create a Tool that the MCP server can expose:

    @Tool(description = "Get all users from a keycloak realm") // [1]
    String getUsers(@ToolArg(description = "A String denoting the name of the realm where the users reside") String realm) { // [2]
        try {
            return mapper.writeValueAsString(userService.getUsers(realm)); // [3]
        } catch (Exception e) {
            throw new ToolCallException("Failed to get users from realm");
        }
    }


1. @Tool(description = “Get all users from a keycloak realm”)

  • @Tool: signifies that the getUsers method is recognized as a callable function or tool by the LLM.
  • description describes the what this Tool does, e.g. get all users. LLM can call this Tool when a request is made to get all users from Keycloak.

Since LLM can understand natural language, all questions leading to a context to get users from Keycloak should *theoretically call this Tool. Also note that if I add too much detail that mixes up with other tool descriptions, the LLM is likely not going to call the desired Tool and eventually hallucinate. Descriptions should be written with care. I suggest to be concise and direct leaving ambiguity and overlaps.

2. ToolArg specifies to the LLM that an argument is required for this Tool. In my case it has to be a realm. So if a user just say get all users. The LLM should come back and ask which realm. Like mentioned above, be mindful about the description parameter.

3.  Finally, once the result comes back from the UserService, in this case a List , I am using the ObjectMapper to convert it to String, so the LLM can understand the response. I have tried this with Jsonb as well, it works pretty okay.

    @Tool(description = "Create a new user in keycloak realm with the following mandatory fields realm, username, firstName, lastName, email, password")  // [1]
    String addUser(@ToolArg(description = "A String denoting the name of the realm where the user resides") String realm,  // [2]
                   @ToolArg(description = "A String denoting the username of the user to be created") String username,
                   @ToolArg(description = "A String denoting the first name of the user to be created") String firstName,
                   @ToolArg(description = "A String denoting the last name of the user to be created") String lastName,
                   @ToolArg(description = "A String denoting the email of the user to be created") String email,
                   @ToolArg(description = "A String denoting the password of the user to be created") String password) {
        return userService.addUser(realm, username, firstName, lastName, email, password);  // [3]
    }


[1] Since this is a create method for a new user. I am specifying exacts of whats required.

[2] All the different parameters required for the successful execution of this Tool.

[3] And finally once the UserService returns, I pass the result to the LLM for further processing.

Packaging

We should add the following properties to the application.properties

quarkus.package.jar.type=uber-jar # [1]
quarkus.log.file.enable=true # [2]
quarkus.log.file.path=kcadmin-quarkus.log # [3]


[1] Tells Quarkus to create an uberjar. An uberjar, also known as a “fat jar” or “shadow jar,” is a type of Java Archive (JAR) file that includes all the dependencies and resources required to run a Java application.

[2] I would also want a log file so I can understand what's going on.

[3] The address and name of the log file.

mvn clean package


The full source code of the application can be found here, in case you would just like to run it. :)

Running With Goose

A local, extensible, open source AI agent that automates engineering tasks

Goose by block provides a CLI tool that has the capability to add MCP as extensions. Use goose configure to add the LLM config and API key.

Once the CLI is configured, I can now add the packaged MCP server by adding it as as an extension.

 goose session --with-extension="java -jar target/keycloak-mcp-server-1.0.0-SNAPSHOT-runner.jar" 


These are examples of questions you might consider asking:

( O)> can I create a new user in keycloak?
Yes, you can create a new user in Keycloak. To do this, you'll need to provide the following information about the user:

- **Realm**: The name of the realm where the user will reside.
- **Username**: The username for the new user.
- **First Name**: The first name of the user.
- **Last Name**: The last name of the user.
- **Email**: The email address of the user.
- **Password**: The password for the user's account.

You can provide these details, and I can assist you with creating the user.


----
( O)> list all users in all realms
Here are the users in the "quarkus" realm:

1. **admin**
   - ID: `af134cab-f41c-4675-b141-205f975db679`

2. **alice**
   - ID: `eb4123a3-b722-4798-9af5-8957f823657a`

3. **jdoe**
   - ID: `1eed6a8e-a853-4597-b4c6-c4c2533546a0`

----
( O)> can you delete user sshaaf from realm quarkus


Okay, so that sums it up. Hope you enjoyed it, and are ready to write your first MCP server implementation as well.

Summary

The article explores creating a practical Model Context Protocol (MCP) server for Keycloak, aiming to learn and demonstrate its potential for AI-powered administration. MCP standardizes how LLMs interact with external tools, prompts, and resources, addressing the issue of fragmented custom integrations. The article details building this experimental Keycloak MCP server using Quarkus and the Keycloak Admin REST client, focusing on user management operations within specified realms. It provides code snippets for a UserService and an MCP UserTool, explaining how to define tools and their arguments for LLM consumption via Stdio. Finally, the article shows how to package the Quarkus application and run it with “Goose,” an AI agent CLI, to interact with Keycloak using natural language queries. The source code for the example can be found here.

Resources

  • Goose - https://github.com/block/goose
  • MCP - https://modelcontextprotocol.io/docs/concepts/tools
  • Keycloak MCP Server - example https://github.com/sshaaf/keycloak-mcp-server
  • MCP and calling your REST APIs - https://github.com/learnj-ai/llm-jakarta/tree/workshop/step-09-mcp
  • Quarkus - https://quarkus.io/blog/mcp-server/
  • Creating MCP Server with Quarkus - https://iocanel.com/2025/03/creating-an-mcp-server-with-quarkus-and-backstage/
Keycloak Quarkus large language model

Published at DZone with permission of Shaaf Syed. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • The Twelve-Factor Agents: Building Production-Ready LLM Applications
  • My Dive into Local LLMs, Part 2: Taming Personal Finance with Homegrown AI (and Why Privacy Matters)
  • Decoding the Secret Language of LLM Tokenizers
  • Master AI Development: The Ultimate Guide to LangChain, LangGraph, LangFlow, and LangSmith

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • [email protected]

Let's be friends: