1. Overview
With the arrival of Large Language Models (LLMs), teams are increasingly integrating AI into their applications. AI integration is no longer limited to simple question-answering. Model Context Protocol is a concept that helps with such integrations.
In this tutorial, we’ll look into building a Model Context Protocol server and client using Quarkus and LangChain4J. We’ll create a simple chatbot and extend the LLM’s capabilities through an MCP server to perform simple custom tasks such as fetching the current date based on a timezone and information about the server JVM.
2. Model Context Protocol 101
Anthropic open-sourced their Model Context Protocol (MCP) in November 2024. MCP provides a standardised way to connect AI-powered applications with external data sources. Before we jump into any code, let’s first appreciate why we need MCP and what it is.
2.1. Why Do We Need Model Context Protocol?
As AI applications become commonplace, expectations to expose organisation and application-specific “context” via LLMs are now growing. Therefore, the integration of such context into the AI workflow needs some consideration as well.
Context can be integrated into the AI workflow from several different data sources, such as databases, file systems, search engines, and other tools. Due to the wide variety of data sources and their underlying connectivity methods, bringing this context into AI integrations poses a significant challenge.
This integration can be baked into AI applications themselves, but that likely increases the complexity of such applications. Besides, many organisations already have a lot of existing internal application services providing a wide variety of information. We ideally want something that allows us the ability to integrate existing services with AI without tight coupling.
2.2. Introducing Model Context Protocol
We understand now the need for some sort of protocol to help with bringing together context from a variety of sources. The Model Context Protocol delivers to this need. Through MCP, we can build complex agents and workflows on top of a native LLM without needing to couple them tightly.
This is analogous to how user interfaces communicate with the backend through the use of a REST API. Once an API contract is established, both the UI and backend can be modified independently so long as the API contract is unchanged.
We note that even though LLMs are trained with a lot of data and have a large volume of information baked into their memory, they aren’t aware of current or proprietary information. A simple way to explore MCP would be to try out some simple services that allow the LLMs to query such information.
Before we dive into code, let’s take a quick look at the MCP architecture and its components:
MCP follows a client-server architecture that includes a few key components:
- MCP Host is our main application that integrates with an LLM and requires connectivity with external data sources
- MCP Clients are components that establish and maintain 1:1 connections with MCP servers
- MCP Servers are components that integrate with external data sources and expose functionalities to interact with them
In order to handle communication between clients and servers, MCP also provides two transport channels:
- Standard Input/Output (stdio) transport type is meant for communicating through Standard input and output streams with local processes and command-line tools.
- Server-Sent Events (SSE) transport type for HTTP-based communication between clients and servers.
MCP is a complex and vast topic; we can refer to the official documentation to learn more.
3. Creating a Custom MCP Server
There are several pre-built MCP servers that we can use with little setup and configuration. However, in this article, we’ll go over the way to create our own MCP server using Quarkus.
For this purpose, we’ll build two simple functional Tools using Quarkus. One tool fetches the current date based on a timezone, and the second provides information about the server JVM.
3.1. Creating the Quarkus MCP Server Project
We require a JDK and Quarkus CLI installed as prerequisites. Once the prerequisites are available, we can create a new project:
quarkus create app --no-code -x qute,quarkus-mcp-server-sse quarkus-mcp-server
This should create a new project under the folder named quarkus-mcp-server with basic scaffolding for a Quarkus project.
3.2. Dependencies
The project created using the quarkus CLI automatically adds core dependencies (quarkus-arc and quarkus-junit5) and the relevant additional dependencies based on the extensions we specify in the command. In our case, those extensions are qute and quarkus-mcp-server-sse:
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkiverse.mcp</groupId>
<artifactId>quarkus-mcp-server-sse</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
</dependencies>
We can now proceed with creating Tools that can be used by the LLM later.
We’ll add a couple of tools. Most LLMs don’t have access to current information without the presence of tools. As such, if we ask an LLM about the current date and time, it will most likely respond with the date and time when the LLM training data was frozen.
So, to test the augmentation of LLM capabilities through the MCP server, we’ll create a simple tool that provides the current date and time in the timezone of the user (or function caller):
@Tool(description = "Get the current time in a specific timezone.")
public String getTimeInTimezone(
@ToolArg(description = "Timezone ID (e.g., America/Los_Angeles)") String timezoneId) {
try {
ZoneId zoneId = ZoneId.of(timezoneId);
ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneId);
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.FULL)
.withLocale(Locale.getDefault());
return zonedDateTime.format(formatter);
} catch (Exception e) {
return "Invalid timezone ID: " + timezoneId + ". Please provide a valid IANA timezone ID.";
}
}
We’ll encapsulate this functional tool inside a plain Java class called ToolBox. The @Tool (io.quarkiverse.mcp.server.Tool) annotation from Quarkus tells the Quarkus framework that the annotated method is to be exposed as a tool in the MCP server.
Also, the @ToolArg (io.quarkiverse.mcp.server.ToolArg) annotation tells the Quarkus framework that the annotated function parameter should be exposed with the provided description.
It’s important to note here that the description filed should clearly specify such information about the function that a caller would need to make proper usage of the function. The description helps the LLM in identifying the purpose of the tool and thus choose which tool should be considered when answering queries from the end user.
We’ll add another function to get information about the JVM in which the MCP server is running:
@Tool(description = "Provides JVM system information such as available processors, free memory, total memory, and max memory.")
public String getJVMInfo() {
StringBuilder systemInfo = new StringBuilder();
// Get available processors
int availableProcessors = Runtime.getRuntime().availableProcessors();
systemInfo.append("Available processors (cores): ").append(availableProcessors).append("\n");
// Get free memory
long freeMemory = Runtime.getRuntime().freeMemory();
systemInfo.append("Free memory (bytes): ").append(freeMemory).append("\n");
// Get total memory
long totalMemory = Runtime.getRuntime().totalMemory();
systemInfo.append("Total memory (bytes): ").append(totalMemory).append("\n");
// Get max memory
long maxMemory = Runtime.getRuntime().maxMemory();
systemInfo.append("Max memory (bytes): ").append(maxMemory).append("\n");
return systemInfo.toString();
}
3.4. Running the Mcp Server
Now that we have a couple of tools available, we can run the Quarkus server in dev mode to test them quickly.
To simplify deployment and development, we’ll package the server as an uber-jar. This makes it possible to mvn install and publish as a JAR to a Maven repository, which makes it easier to share and run for us and others.
By default, Quarkus uses the HTTP port 8080. So, we’ll change that 9000 since we’ll later require the 8080 port for use with the MCP client package. We make both these changes in the application.properties file:
quarkus.package.jar.type=uber-jar
quarkus.http.port=9000
We can now run the Quarkus dev mode using the quarkus CLI. Once the dev mode server is up and running, we expect it to be running at http://localhost:9000/q/dev-ui/:
quarkus dev
Since we used the @Tool annotation to expose our function as a part of the MCP server, the dev mode automatically adds an MCP server extension for us to use. This allows us to quickly test the tools using an Open API / Postman style tool calling interface provided by the Quarkus dev mode.
We’ll now use the MCP server extension from the Quarkus Dev UI to run some quick tests.
First, we navigate to the tools tab under Dev UI. We can click on the Tools link under the MCP server extension:
It’s also available at the URL http://localhost:9000/q/dev-ui/io.quarkiverse.mcp.quarkus-mcp-server-sse/tools. We note that both the tools we’ve defined are visible for running a quick test using the Call action button:
Let’s call the getTimeInTimezone tool by providing it with a particular timezone. The tool caller already offers a pre-filled JSON based on our description. We modify it to provide some real value:
{
"timezoneId": "Asia/Kolkata"
}
With the input filled, we click the Call button, and immediately the response section provides appropriate output in JSON format:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"isError": false,
"content": [
{
"text": "Sunday, May 18, 2025, 7:33:26 PM India Standard Time",
"type": "text"
}
]
}
}
Similarly, we can test the other function as well. Now that we’ve got the server side ready, let’s move to the client side.
4. Creating a Custom MCP Client
We’ve got an MCP server ready with a couple of custom tools. It’s time to open up those tools for our chat with an LLM. For this purpose, we’ll be using Quarkus and LangChain4j. For the LLM API, we’ll use Ollama running on a local machine and Mistral as the model. However, we can also use any other LLM supported by the LangChain4j library.
4.1. Creating the Quarkus MCP Client Project
We can use the quarkus cli to create a new project:
quarkus create app --no-code -x langchain4j-mcp,langchain4j-ollama,vertx-http quarkus-mcp-client
This should create a new project under the folder named quarkus-mcp-client with basic scaffolding for a Quarkus project.
4.2. Dependencies
The project created using the quarkus CLI automatically adds core dependencies (quarkus-arc and quarkus-junit5) and the relevant additional dependencies based on the extensions we specify in the command. In our case, those extensions are langchain4j-mcp, langchain4j-ollama, and vertx-http:
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-mcp</artifactId>
<version>1.0.0.CR2</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-ollama</artifactId>
<version>1.0.0.CR2</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-http</artifactId>
</dependency>
4.3. Defining a Chatbot
We’ll leverage Quarkus and LangChain4j to define a Chat service with a custom System Prompt.
We create an interface called McpClientAI by annotating it with the @RegisterAiService annotation. Quarkus uses this annotation to automatically generate LLM client services using LangChain4j and a configured LLM:
@RegisterAiService
@SessionScoped
public interface McpClientAI {
@SystemMessage("""
You are a knowledgeable and helpful assistant powered by Mistral.
You can answer user questions and provide clear, concise, and accurate information.
You also have access to a set of tools via an MCP server.
When using a tool, always convert the tool's response into a natural, human-readable answer.
If the user's question is unclear, politely ask for clarification.
If the question does not require tool usage, answer it directly using your own knowledge.
Always communicate in a friendly and professional manner, and ensure your responses are easy to understand.
"""
)
@McpToolBox("default")
String chat(@UserMessage String question);
}
In addition, we attach a custom System Prompt to the chat() method using the @SystemMessage annotation. This System Prompt suggests to the LLM that it has access to tools via an MCP server and that it should use them to respond to user queries.
We tell Quarkus that an MCP server is configured with the name “default” using the @McpToolBox annotation. Quarkus should then use it with the McpClientAI services.
Now, we need to tell Quarkus where to find the LLM and Tools for using the AI service.
Quarkus offers many configuration options to automatically configure the LLM client as well as the MCP client. We need to set up some of those properties in the application.properties file of Quarkus to wire our LLM and MCP server.
First, we set up a timeout for any calls to LLMs. This is an optional setting, but may need to be configured to a larger value if running the LLMs on slower machines (such as a developer desktop):
quarkus.langchain4j.timeout=10s
For our test, we’ll run an Ollama server on our local machine at port 11434, and we’ll load the Mistral model on the Ollama server. We inform Quarkus about that through appropriate properties:
quarkus.langchain4j.chat-model.provider=ollama
quarkus.langchain4j.ollama.chat-model.model-id=mistral
quarkus.langchain4j.ollama.base-url=http://localhost:11434
Next, we point Quarkus to the MCP server we created earlier. This server is running on port 9000 per our configuration. In addition, we note that the Quarkus MCP server is automatically configured at the URI /mcp/sse:
quarkus.langchain4j.mcp.default.transport-type=http
quarkus.langchain4j.mcp.default.url=http://localhost:9000/mcp/sse
With this configuration, Quarkus automatically sets up the connection to the MCP server and makes the tools available for use by our Chat services. The property names contain the word “default”, which should match the name that we have configured in the @McpToolBox annotation parameters. We can now run the Quarkus dev mode using the Quarkus CLI. Once the dev mode server is up and running, we expect it to be running at http://localhost:8080/q/dev-ui/:
quarkus dev
Since we used the @RegisterAiService annotation to create a chat service, the dev mode automatically adds an extension called LangChain4j core for us to use. This allows us to quickly test the chat client without needing to write additional services. The LangChain4j code extension offers a ready-to-use chat UI that connects to our chat service.
4.5. Testing the Chat Service
We’ll now use the chat UI extension from the Quarkus Dev UI to run some quick tests. First, we navigate to the tools tab under Dev UI. We can click on the Chat link under the LangChain4j Core extension. It’s also available at the URL http://localhost:8080/q/dev-ui/io.quarkiverse.langchain4j.quarkus-langchain4j-core/chat.
We can chat with the LLM using this tool. We use questions in plain English to inquire about Time and System Information, and the LLM now uses the tools provided by our MCP server to provide up-to-date information.
A sample chat looks something like:
Me
hi
AI
Hello! How can I assist you today? If you need help with a specific task, feel free to ask and I’ll do my best to help. Here are some examples of what I can do: – Get system information such as available processors, free memory, total memory, and max memory using `getSystemInfo()`. – Get the current time in a specific timezone using `getTimeInTimezone(timezoneId)`, where `timezoneId` is a string representing the desired timezone (e.g., “America/Los_Angeles”). Let me know if you have any questions or need further assistance!
Me
Give me system information
Tools
Request to execute the following tools: Request ID = undefined, tool name = getSystemInfo, arguments = { }
Tools
Tool execution result for request ID = undefined, tool name = getSystemInfo, status = Available processors (cores): 8 Free memory (bytes): 94046840 Total memory (bytes): 182452224 Max memory (bytes): 4171235328
AI
Here is the system information for your device: – Number of available processors (cores): 8 – Free memory: 94,046,840 bytes – Total memory: 182,452,224 bytes – Maximum memory: 4,171,235,328 bytes You can use this information in your program as needed.
Me
What time is it in India right now
Tools
Request to execute the following tools: Request ID = undefined, tool name = getTimeInTimezone, arguments = { “timezoneId” : “Asia/Kolkata” }
Tools
Tool execution result for request ID = undefined, tool name = getTimeInTimezone, status = Saturday, May 10, 2025, 4:26:37 PM India Standard Time
AI
The current time in India (India Standard Time) is: Saturday, May 10, 2025 at 4:26:37 PM.
That’s it, we’ve now built a chat service that includes custom tools. We can use this service now as a building block to develop additional services, such as an HTTP API for chatting with an LLM.
It’s important to note, however, that even though the tools are available, whether they will be used depends on the capabilities of the LLM. Sometimes, the LLM may simply ignore the tool even when it’s available to assist with the answer. Therefore, there may be a need to tune the System Prompts and Tool descriptions to help the LLM identify when to use a tool.
5. Conclusion
In this article, we configured an MCP server using Quarkus and a separate MCP client application that connects to this MCP server. We used Quarkus and LangChain4j to build the client application. Behind the scenes, we wired it to use the Ollama API to connect to the Mistral LLM.
We augmented the capabilities of the Mistral LLM by providing it with tools to get JVM system information and the current time. Without tools, this dynamic information isn’t available to the LLM. These tools that we used in this article are unlikely to be tools used in real projects. That said, they do demonstrate the possibility of augmenting the LLM capabilities by providing our own custom tools.
Finally, we note that the MCP server allows us to provide a variety of features using our own code, and an MCP client allows us to easily integrate these tools into an LLM-connected application.
As always, the code is available over on GitHub.