The conversation around Generative AI has been dominated by what Large Language Models (LLMs) know. Their ability to process vast amounts of text and answer complex questions is impressive, but for any enterprise, knowledge alone is not enough. The real value lies in what an application can do. An LLM, on its own, is like a brilliant new hire with an encyclopedic memory but no access to the company’s tools; it can’t check inventory, update a customer record, or process an order. The systems that perform these critical actions (the systems of record, the databases, and the core business engines) are overwhelmingly built and maintained by Java developers.
This gap between the AI’s knowledge and the enterprise’s ability to act represents a massive opportunity for the Java community. AI needs “hands” to interact with the real world, and Java developers are perfectly positioned to build them. By exposing core business functions as secure, standardized tools, developers can empower AI agents to perform meaningful, revenue-generating tasks.
This is where the Model Context Protocol (MCP) enters the scene. Think of MCP as a universal standard, the “USB-C port for AI applications”. It is not another complex framework to learn but rather an open, standardized contract for communication. MCP allows an AI to securely discover and use external tools, transforming it from a passive knowledge base into an active participant in business processes.
In this hands-on guide, we will construct a secure MCP server using Quarkus to expose a piece of mock business logic. Then, we will build an intelligent AI assistant with Langchain4j that consumes this tool to answer user queries.
Demystifying the Model Context Protocol (MCP)
At its heart, MCP is a contract, not a library. It is a specification, much like HTTP, that defines a standard way for systems to communicate. This approach fundamentally decouples the AI application (the client) from the tool or data provider (the server). This is a foundational architectural principle that resonates deeply with enterprise developers familiar with Service-Oriented Architecture or microservices. The protocol itself is built upon JSON-RPC 2.0, a lightweight and familiar remote procedure call protocol that makes its messages human-readable and easy to debug.
The Core Architecture for Java Developers
The MCP architecture is based on a classic client-server model, with clear roles that map directly to enterprise Java development patterns.
MCP Server
This is where the enterprise logic resides. As a Java developer, your primary role in an MCP ecosystem is to build servers that wrap existing business services. This could be a facade over a database repository, a client for a legacy mainframe system, or an adapter for another microservice. The MCP server exposes specific methods from these services as “Tools” that an AI client can discover and execute. It acts as a secure and well-defined gateway to your organization’s core business capabilities.
MCP Client
This is the AI-powered component of the application. In our example, the client will be managed by the Langchain4j library. The client’s responsibility is to connect to one or more MCP servers, discover the tools they offer, and present those tool specifications to an LLM. The LLM then uses these specifications as part of its context to reason about how to respond to a user’s prompt. If the LLM decides a tool is needed, the client facilitates the execution of that tool on the server.
Transport Layer
MCP defines two primary transport mechanisms for communication between the client and server, offering flexibility for different deployment scenarios.
stdio: This mechanism uses standard input and standard output for communication. It is ideal for local-first applications and agentic systems where the client can launch and manage the server as a local subprocess. This transport offers the highest performance and simplest setup, as it avoids network overhead entirely.Streamable HTTP: This mechanism uses HTTP with Server-Sent Events (SSE) for communication. So the MCP server is deployed as a remote microservice. It allows us to build scalable and decoupled architectures where AI clients across the network can connect to a centralized tool server. We will use this method in our example to create a production-oriented architecture.
MCP as a Strategic Decoupling Pattern
The true power of MCP for enterprise architecture lies in its role as a strategic decoupling pattern. Traditional AI integrations often require writing custom client libraries for every service the AI needs to call. This creates a hard-to-maintain, tightly coupled system; if the service’s API changes, the AI application breaks. While patterns like Retrieval-Augmented Generation (RAG) are powerful, they are primarily designed for reading static data and do not inherently support performing actions or transactions.
MCP introduces a standardized intermediary layer that solves these problems. The AI application (the client) does not need to know how an inventory level is checked; it only needs to discover that a tool named checkStockLevel exists and understand the parameters it requires. The MCP server encapsulates the implementation details, whether that involves a complex SQL query, a call to a SOAP service, or an interaction with a proprietary ERP system.
This decoupling allows the AI/LLM part of the stack and the enterprise systems part of the stack to evolve independently. A Java team can refactor, upgrade, or completely replace their backend inventory system, and as long as the MCP tool contract exposed by the server remains consistent, the AI agent continues to function without any modification. This principle is absolutely critical for ensuring long-term maintainability and agility in large, complex organizations.
Assembling Our Modern Java AI Stack
Before we dive into the code, let’s set up our development environment and scaffold the projects. We will be creating two distinct projects to emphasize the decoupled nature of the client-server architecture.
Project Scaffolding with Maven
Open your terminal and run the following commands to create two separate Maven projects. This physical separation is key to demonstrating the architectural decoupling that MCP provides.
First, create the MCP server project:
mvn io.quarkus.platform:quarkus-maven-plugin:3.26.2:create \
-DprojectGroupId=com.eldermoraes \
-DprojectArtifactId=inventory-mcp-server \
-Dextensions='quarkus-mcp-server-sse'
Next, create the AI assistant client project:
mvn io.quarkus.platform:quarkus-maven-plugin:3.26.2:create \
-DprojectGroupId=com.eldermoraes \
-DprojectArtifactId=assistant-mcp-client \
-Dextensions='quarkus-langchain4j-mcp,quarkus-langchain4j-ollama,quarkus-rest'
Building the MCP Server (A Gateway to Business Logic)
Our first task is to build the MCP server. This server will simulate a connection to a company’s inventory management system. It will expose a single, critical business function: checking the stock level of a specific product. This serves as a secure and controlled gateway to our mock business logic.
The Tool Implementation
Create a new Java class in the inventory-mcp-server project. This class will contain the method we want to expose as an MCP tool.
package com.eldermoraes;
import io.quarkiverse.mcp.server.Tool;
import io.quarkiverse.mcp.server.ToolArg;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.Map;
@ApplicationScoped
public class ProductInventoryTool {
// Simulate a connection to a product inventory database.
private static final Map<String, Integer> INVENTORY_DATA = Map.of(
"QK-123", 127,
"LC-456", 58,
"JV-789", 0
);
@Tool(description = "Checks the current stock level for a given product ID.")
public String checkStockLevel(
@ToolArg(description = "The unique identifier of the product (e.g., 'QK-123').")
String productId) {
if (productId == null || productId.isBlank()) {
return "Error: Product ID cannot be null or empty.";
}
Integer stockLevel = INVENTORY_DATA.get(productId);
if (stockLevel == null) {
return String.format("Product with ID '%s' was not found in the inventory.", productId);
} else if (stockLevel == 0) {
return String.format("Product '%s' is currently out of stock.", productId);
} else {
return String.format("Product '%s' has %d units in stock.", productId, stockLevel);
}
}
}
Code Explanation
@ApplicationScoped: This is a standard Jakarta Contexts and Dependency Injection (CDI) annotation that declares this class as a managed bean with a single instance for the application’s lifecycle.@Tool: This annotation, provided by thequarkus-mcp-serverextension, is the key to the integration. It signals that this method,checkStockLevel, should be exposed as a discoverable MCP tool. Thedescriptionparameter is critically important. This natural language description is what the LLM will use to understand the tool’s purpose and decide when to use it.@ToolArg: This annotation is used for each parameter of the tool-enabled method. Similar to@Tool, itsdescriptionis vital for the LLM to correctly map information from a user’s prompt to the function’s arguments.- Simulated Database: We use a simple
private static final Mapto represent our inventory data. This keeps the example self-contained and focused on the MCP mechanics rather than on database configuration.
Server Configuration
With the quarkus-mcp-server-sse extension, the server will run as a standard Quarkus web application. No special configuration is needed in application.properties for the server to function. It will automatically start on the default HTTP port (8080) and expose the necessary MCP endpoints.
Implementing the MCP Client (The AI Assistant)
With the server ready, we now turn to building the “brains” of the operation. Our client application will be a conversational AI assistant. This assistant will have no direct knowledge of inventory systems, databases, or product IDs. Its only specialized skill will be its ability to connect to our configured MCP server and use the tools it discovers there.
Connecting to the Server
The connection between the client and the server is defined declaratively in the client’s application.properties file. This is where the power of the quarkus-langchain4j-mcp extension becomes apparent.
quarkus.http.port=8081
# Configure the Langchain4j MCP client. We give our connection the name "inventory".
quarkus.langchain4j.mcp.inventory.transport-type=http
# Define the URL of the running MCP server's Server-Sent Events (SSE) endpoint.
# This assumes the server is running locally on port 8080.
quarkus.langchain4j.mcp.inventory.url=http://localhost:8080/mcp/sse
# Configure the Ollama chat model.
# This assumes Ollama is running locally on its default port.
quarkus.langchain4j.ollama.chat-model.model-id=gpt-oss:20b
Configuration Explanation
quarkus.langchain4j.mcp.inventory.transport-type=http: This property tells thequarkus-langchain4j-mcpextension that we are defining an MCP client configuration named “inventory” and that it should use thehttptransport to communicate.quarkus.langchain4j.mcp.inventory.url=http://localhost:8080/mcp/sse: The URL for the running MCP server. The client will connect to this endpoint to discover and execute tools.quarkus.langchain4j.ollama.chat-model.model-id=gpt-oss:20b: This is the standard configuration for the Langchain4j Ollama integration, instructing it to use thegpt-oss:20bmodel, which must be available in your local Ollama instance.
Defining the AI Service
Next, we define the AI assistant itself using a simple Java interface. Langchain4j’s AI Services feature will automatically provide the implementation.
package com.eldermoraes;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.quarkiverse.langchain4j.mcp.runtime.McpToolBox;
@RegisterAiService
public interface InventoryAssistant {
@McpToolBox("inventory")
String chat(@MemoryId Object session, @UserMessage String message);
}
Code Explanation
@RegisterAiService: This is a powerful annotation from theio.quarkiverse.langchain4jpackage. It automatically generates a CDI bean that implements this interface, wiring it to the configured LLM (a local Ollama model in this case) and any specified tools.@McpToolBox("inventory"): This is the declarative magic from theio.quarkiverse.langchain4j.mcp.runtimepackage that connects our assistant to the MCP server. It instructs the AI Service to inject all available tools discovered from the MCP client that we configured under the name “inventory” inapplication.properties. With this single annotation, our assistant automatically gains thecheckStockLevelcapability without having any direct code dependency on theProductInventoryToolclass or the server project.
Exposing the Assistant via a REST Endpoint
Finally, to make our assistant interactive, we will expose it through a simple JAX-RS REST endpoint. This allows us to communicate with it using standard tools like curl.
package com.eldermoraes;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.UUID;
@Path("/chat")
public class AssistantResource {
@Inject
InventoryAssistant assistant;
@POST
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
public String chat(String message) {
// Use a unique session ID for each request for simplicity.
// In a real application, this would be tied to a user session.
return assistant.chat(UUID.randomUUID(), message);
}
}
Code Explanation
- We use
@Injectto get an instance of ourInventoryAssistantinterface, which Quarkus has automatically implemented for us. - We create a simple
POSTendpoint at/chatthat accepts a plain text message from the user and returns the assistant’s plain text response.
Running the Full System
With both the server and client components built, it is time to see them work together. Because we are now using a distributed architecture, we need to run both applications simultaneously.
Run Sequence
You will need two separate terminal windows for this process.
- Run the Server: In your first terminal, navigate to the server’s directory and start it in development mode. It will run on port 8080.
mvn clean quarkus:dev - Run the Client: In your second terminal, navigate to the client’s directory and start it.
mvn clean quarkus:dev
Verification
You should see standard Quarkus startup logs in both terminals, indicating that two separate applications are running. The client application’s logs will show that it is connecting to the MCP server at http://localhost:8080/mcp/sse.
Testing the Assistant with curl
Once both applications are running, open a new (third) terminal to interact with your AI assistant.
Example 1: Non-Tool-Using Query
Let’s start with a general question that does not require any of our specialized tools.
curl -X POST -H "Content-Type: text/plain" -d "Hello, what can you do?" http://localhost:8081/chat
In this case, the request goes directly to the LLM. The model will formulate a response based on its general knowledge.
Example 2: Tool-Using Query
Now for the main event. Let’s ask a question that requires our assistant to use its specialized tool.
curl -X POST -H "Content-Type: text/plain" -d "How many units of product 'QK-123' do we have in stock?" http://localhost:8081/chat
This simple request triggers a sophisticated, multi-step process orchestrated by Langchain4j and MCP:
- The client application sends the user’s prompt, along with the specification for the
checkStockLeveltool, to the local LLM served by Ollama. - The LLM analyzes the prompt and determines that the
checkStockLeveltool is the appropriate function to call. It also extracts theproductIdparameter, ‘QK-123’. - The LLM responds with a special message indicating that it wants to execute a tool call with the specified parameters.
- The Langchain4j framework intercepts this request. It then invokes the
checkStockLeveltool via the MCP client, which sends a JSON-RPC request over HTTP to the runninginventory-mcp-serverapplication. - The MCP server receives the request, executes the
checkStockLevelmethod, and returns the result as a JSON-RPC response:"Product 'QK-123' has 127 units in stock.". - Langchain4j receives this result and sends it back to the LLM as additional context.
- The LLM now has the complete context and formulates a final, human-readable answer.
The final output you see in your terminal will be a clear, concise answer:
We have 127 units of product 'QK-123' in stock.
Java’s Strategic Role in the Agentic AI Future
In this guide, we have successfully built a complete AI-powered application. The architecture we created features a completely decoupled AI assistant and business logic service, communicating over the network using the open MCP standard. This is a powerful and maintainable pattern for building enterprise-grade AI applications that mirrors modern microservice architectures.
This pattern extends far beyond simple chatbots. It is the foundation for building Agentic AI systems, a topic of growing importance in the industry. This is about creating autonomous agents that can use a suite of tools to perform complex, multi-step business processes. In this new paradigm, Java developers are not just writing backends; they are building the “real-world toolsets” that these intelligent agents will use to operate.
Consider the systems within your own organization. What core business functions, currently locked away behind internal APIs, could be exposed as secure MCP tools? How could an AI agent, armed with tools to query customers, process orders, and check logistics, fundamentally change how your business operates?
The journey does not end here. The logical next steps are to:
- Deploy the MCP server as a containerized microservice, making your tools available across your network.
- Implement more complex tools that involve database transactions, orchestrate calls to other APIs, or interact with legacy systems.
- Investigate the growing ecosystem of pre-built MCP servers for common tasks, such as interacting with GitHub or the local filesystem, which can be easily integrated into your applications.
The era of AI is not a threat to the Java ecosystem; it is its next great opportunity. Java developers, as the architects and guardians of enterprise logic, are essential to building the robust, secure, and intelligent systems of the future. Open standards and patterns like MCP are the key to unlocking that potential, ensuring that Java remains the engine of the enterprise for decades to come.
If you run Java at scale, grab the free whitepaper “The Enterprise Guide to AI in Java (POC to Production)”. Download it here.
Hi Elder, great article!
I have a question about the use of the @Tool annotation. If you have a larger number of tools with slightly different purposes, which could potentially be confused with one another, is it a good practice to use a very detailed description to address the non-deterministic nature of the LLM, or is this a practical approach that should be avoided?
Thanks for sharing!
I think it’s the same care you should take when building any APIs. But yes, a good prompt (via @SystemMessage or @UserMessage) will help to address it.