LangChain4j MCP Client: Connect Java to an MCP Server
Introduction
To connect a Java application to a Model Context Protocol (MCP) server with the LangChain4j MCP client, you add the langchain4j-mcp module, create a transport (STDIO for a server that runs as a local subprocess, Streamable HTTP for a server you reach over the network), and wrap it in a DefaultMcpClient.
You then pass the client to an McpToolProvider, and any LangChain4j AI service can call the server's tools.
MCP provides a standardized way for artificial intelligence (AI) models to interact with external tools and systems. Check my introduction to MCP if you're new to the protocol.
In this post, I'll show you how to connect your Java applications to an MCP server using LangChain4j, enabling your AI agents to perform real-world actions like managing Kubernetes Pods.
I'll cover both the STDIO and Streamable HTTP transports, which protocol version to use with each, how to limit the tools the model can use, and how I use LangChain4j to test MCP server implementations.
I tested every step in October 2026 with LangChain4j 1.22.0 (langchain4j-mcp 1.22.0-beta32), Temurin JDK 25, the Kubernetes MCP Server v0.0.67, and a small local model (Qwen3 4B) running in Ollama, so you can follow along without an API key.
Which LangChain4j version do you need?
LangChain4j shipped its first MCP client in version 1.0.0-alpha1 (December 2024), a month after Anthropic introduced the protocol, and I first wrote this post in March 2025 with 1.0.0-beta1. Since then, the MCP client changed enough that code from older tutorials may not compile with a current release:
| LangChain4j version | MCP client changes |
|---|---|
| 1.0.0-alpha1 (Dec 2024) | First MCP client, with the STDIO and SSE (HttpMcpTransport) transports |
| 1.4.0 (Aug 2025) | StreamableHttpMcpTransport |
| 1.6.0 (Sep 2025) | executeTool() returns ToolExecutionResult instead of String |
| 1.19.0 (Aug 2026) | MCP 2026-07-28 support and protocol detection; HttpMcpTransport (SSE) removed |
| 1.22.0 (Oct 2026) | Latest release as of October 2026, used in this post |
If your build fails with cannot find symbol: class HttpMcpTransport after an upgrade, replace it with StreamableHttpMcpTransport and point it at the server's Streamable HTTP endpoint, as shown below.
McpClient.executeTool() also returns a ToolExecutionResult instead of a String since 1.6.0; call resultText() on it to get the text.
Setting up LangChain4j MCP clients
LangChain4j provides a flexible MCP client API for connecting to MCP servers through different transport mechanisms. The client sits between your Java application and the MCP server: it starts or connects to the server, lists its tools, and calls them.
Adding the LangChain4j MCP dependency
LangChain4j needs Java 17 or later.
The MCP client lives in its own module, langchain4j-mcp, which is still published with a -beta suffix as of October 2026 (1.22.0-beta32 is the one that matches LangChain4j 1.22.0).
The LangChain4j BOM picks the matching version for you, so with Maven you only need to import it and declare the modules:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-bom</artifactId>
<version>1.22.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
</dependencies>With Gradle, the equivalent is implementation(platform("dev.langchain4j:langchain4j-bom:1.22.0")) plus the two modules without a version.
langchain4j-ollama is the model provider I use later in this post; swap it for the provider you want.
The MCP specification defines two standard transports: Standard Input/Output (STDIO) and Streamable HTTP. Let's check out how to set up each one.
Setting up an STDIO transport client
The STDIO transport method launches an MCP server as a subprocess and communicates with it through standard input/output streams. This method is most suitable for local execution which is convenient for agents that perform tasks on the same machine where the server is running. Managing local resources, executing shell commands, or interacting with local services are common use cases for STDIO transport.
The following code snippet demonstrates how to set up an STDIO transport client in LangChain4j:
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
public final class LangChainMcpClient {
static McpClient initStdioClient(String... command) {
return DefaultMcpClient.builder()
// Optional client name to identify with the server, defaults to "langchain4j"
.clientName("blog.marcnuri.com")
// Optional timeout for each individual tool execution, defaults to 60 seconds
.toolExecutionTimeout(Duration.ofSeconds(30))
.transport(StdioMcpTransport.builder()
// The command that starts the MCP server
.command(List.of(command))
// Optional, log the MCP messages (DEBUG level of the "MCP" logger)
.logEvents(true)
.build())
.build();
}
}These are the key components of the STDIO transport client setup:
clientName: An optional parameter to identify the client with the server. If not specified, the client name defaults tolangchain4j.toolExecutionTimeout: An optional parameter to set the timeout for each tool execution. If not specified, the timeout defaults to 60 seconds.StdioMcpTransport.command: The command to execute the MCP server. This should be a List of strings representing the command and its arguments, for exampleinitStdioClient("npx", "-y", "kubernetes-mcp-server@latest"). On Windows, usenpx.cmdinstead ofnpx.StdioMcpTransport.logEvents: An optional parameter to enable logging of the MCP messages. LangChain4j logs them at DEBUG level with a logger namedMCP, so you also need to enable that logger in your logging configuration.
The subprocess inherits your application's whole environment, so the Kubernetes MCP Server finds your KUBECONFIG and proxy variables without any extra configuration.
It also receives any API keys or tokens in that environment, so only run servers you trust.
If the server needs a variable your application doesn't have, add it with the transport's environment(Map<String, String>) method.
Which MCP protocol version does the client use?
Since LangChain4j 1.19.0, the client detects the protocol version when it connects: it sends the server/discover request of the 2026-07-28 revision, which replaced the initialize handshake with metadata that travels with every request, and falls back to the classic initialize handshake (2025-11-25) if the server answers with an error.
protocolVersion("2026-07-28") or protocolVersion("2025-11-25") on the client builder skips the detection and forces a version.
Over STDIO, the default detection works: the Kubernetes MCP Server v0.0.67 supports the 2026-07-28 revision, and that's what the client negotiated in my tests. Over Streamable HTTP, it depends on the server, as the next section explains.
Setting up a Streamable HTTP transport client
The Streamable HTTP transport method establishes a connection with the MCP server over HTTP using a single endpoint for bidirectional messaging. This method is suitable for remote execution where the server is running on a different machine or in a cloud environment. Interacting with web services, managing cloud resources (such as Kubernetes), or executing long-running tasks are common use cases for Streamable HTTP transport.
Note
Prior to MCP protocol version 2025-03-26, the SSE (Server-Sent Events) transport was used for remote connections.
SSE is now deprecated in favor of Streamable HTTP, and LangChain4j removed its SSE transport (HttpMcpTransport) in version 1.19.0.
To try it locally, start the Kubernetes MCP Server in HTTP mode in a separate terminal:
npx -y kubernetes-mcp-server@latest --port 8080 --bind-address 127.0.0.1Without --bind-address, the server listens on all network interfaces (0.0.0.0) and doesn't authenticate its clients by default, so anyone who can reach the port can use your cluster with your kubeconfig credentials.
The following code snippet demonstrates how to set up a Streamable HTTP transport client in LangChain4j:
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport;
public final class LangChainMcpClient {
static McpClient initHttpClient(String url) {
return DefaultMcpClient.builder()
.clientName("blog.marcnuri.com")
.toolExecutionTimeout(Duration.ofSeconds(30))
// Skip protocol detection: over HTTP, the Kubernetes MCP Server speaks 2026-07-28 only with --stateless
.protocolVersion("2025-11-25")
.transport(StreamableHttpMcpTransport.builder()
// The URL of the MCP server's endpoint
.url(url)
// Optional, log the MCP requests and responses (INFO level of the "MCP" logger)
.logRequests(true)
.logResponses(true)
.build())
.build();
}
}These are the key components of the Streamable HTTP transport client setup:
protocolVersion: Forces the 2025-11-25 protocol and skips the detection. I explain why below.StreamableHttpMcpTransport.url: The URL of the MCP server's endpoint,http://127.0.0.1:8080/mcpfor the server we just started.StreamableHttpMcpTransport.logRequestsandlogResponses: Optional parameters to log the MCP request and response messages.- The builder also accepts
customHeaders(Map<String, String>), for example to send anAuthorizationheader to a remote server, which should also be HTTPS-enabled.
To use it, pass initHttpClient("http://127.0.0.1:8080/mcp") to the assistant method below instead of the STDIO client.
Warning
Over HTTP, the Kubernetes MCP Server only supports the 2026-07-28 revision when it runs with --stateless; by default, it answers the client's server/discover request with the older versions it supports.
LangChain4j 1.22.0 doesn't check that list: it switches to the 2026-07-28 protocol anyway, and every following request fails with HttpException: Unexpected status code: 400.
Setting protocolVersion("2025-11-25") avoids the problem with any server; if you run the server yourself, starting it with --stateless also works, at the cost of its tool and prompt change notifications.
Example: a Java AI agent that uses the Kubernetes MCP Server
Once you have your MCP client set up, you can integrate it with LangChain4j's AI services to create an assistant that can interact with the real world.
The examples use a local model through Ollama. Install Ollama and pull a small model that supports tool calling (about 2.5 GB):
ollama pull qwen3:4b-instructThe Kubernetes MCP Server also needs a cluster to talk to.
It uses your default kubeconfig file, so any cluster works; I used a local kind cluster with an nginx Deployment of two replicas.
The examples start the server with npx, so you also need Node.js; npx downloads the server binary for your platform.
Here's how to create a very simple assistant that can manage a Kubernetes cluster by using the Kubernetes MCP Server:
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.tool.ToolArgumentsErrorHandler;
import dev.langchain4j.service.tool.ToolExecutionErrorHandler;
public final class LangChainMcpClient {
interface Assistant {
String chat(String userMessage);
}
static Assistant assistant(McpClient client) {
return AiServices.builder(Assistant.class)
.chatModel(OllamaChatModel.builder()
.baseUrl("http://localhost:11434")
.modelName("qwen3:4b-instruct")
.timeout(Duration.ofMinutes(5))
.build())
.toolProvider(McpToolProvider.builder().mcpClients(client).build())
.toolArgumentsErrorHandler(ToolArgumentsErrorHandler.sendExceptionMessageToLlm())
.toolExecutionErrorHandler(ToolExecutionErrorHandler.failInvocationUnlessVisibleToLlm())
.build();
}
public static void main(String[] args) throws Exception {
// On Windows, use "npx.cmd"
try (var client = initStdioClient("npx", "-y", "kubernetes-mcp-server@latest")) {
final var assistant = assistant(client);
System.out.println(assistant.chat("Run a Pod with the image marcnuri/chuck-norris and expose port 8080"));
System.out.println(assistant.chat("List all pods in my cluster and output their name and namespace as a Markdown table"));
}
}
}In this example, we create a very simple assistant that connects the Kubernetes MCP Server tools with the Qwen3 4B model running in Ollama.
These are the key components of the assistant integration example:
Assistant:
The simplest assistant interface that provides achatmethod to interact with the AI model. LangChain4j's AI services use this interface to automatically create the assistant implementation.OllamaChatModel:
A chat model that sends the requests to a local Ollama server. We could use any other model andChatModelimplementation provided by LangChain4j, as long as the model supports tool calling.baseUrl: The URL of the Ollama server (Ollama listens onhttp://localhost:11434by default); required.modelName: The name of the model to use.timeout: Small models on a laptop can take a while to answer, so I raised it from the default 60 seconds.
McpToolProvider:
A tool provider that uses the MCP client to execute tools on the MCP server. ThemcpClientsmethod accepts one or more MCP clients to use for tool execution.toolArgumentsErrorHandlerandtoolExecutionErrorHandler:
As of LangChain4j 1.22.0, an AI service with tools logs a warning unless you choose these handlers explicitly, because their defaults are planned to change. These two are the recommended ones: the model sees its own argument errors and the errors the MCP server reports, and any other exception fails the call instead of reaching the model.
Tip
Switching to a hosted model only changes the chatModel.
For example, with the langchain4j-google-ai-gemini module, you'd use GoogleAiGeminiChatModel.builder().apiKey(System.getenv("GOOGLE_API_KEY")).modelName("<model-id>").build().
In the main(String[]) method, we start the MCP server over STDIO, create an assistant instance with the assistant method, and send it two messages.
The try-with-resources block closes the client, which also stops the server process.
The complete example is a single file you can run with JBang: jbang LangChainMcpClient.java --assistant.
For the first message, we ask the assistant to run a Pod with the image marcnuri/chuck-norris and expose port 8080.
The model called the server's pods_run tool with the image and the port, and this is the output of the assistant's response (I left out its last two paragraphs, which suggest next steps):
The Pod and Service have been successfully created and exposed on port 8080. Here are the details:
- **Pod Name**: `kubernetes-mcp-server-run-hlzqz`
- **Namespace**: `default`
- **Image**: `marcnuri/chuck-norris`
- **Port**: `8080` (exposed via container port)
- **Service Name**: `kubernetes-mcp-server-run-hlzqz`
- **Type**: `ClusterIP`
- **ClusterIP**: `10.96.78.72`
- **Port**: `8080` (TCP)
- **Selector**: Matches the pod label `app.kubernetes.io/name: kubernetes-mcp-server-run-hlzqz`
The Pod is currently in the **Pending** phase, which means it is waiting for a node to be assigned. Once assigned, it will start running and serve requests on port 8080.The details come from the tool's result, but the explanation of the Pending phase is the model's own, and it's only half right: the tool returns the Pod the moment it creates it, and every new Pod stays Pending until its containers start, which includes waiting for a node and pulling the image.
For the second message, we ask the assistant to list the Pods in the cluster as a Markdown table. This is the output of the assistant's response:
| NAME | NAMESPACE |
| --- | --- |
| kubernetes-mcp-server-run-hlzqz | default |
| nginx-5869d7778c-8spml | default |
| nginx-5869d7778c-c2hhm | default |
| coredns-668d6bf9bc-l6qbl | kube-system |
| coredns-668d6bf9bc-trd6s | kube-system |
| etcd-lc4j-refresh-control-plane | kube-system |
| kindnet-lj6l8 | kube-system |
| kube-apiserver-lc4j-refresh-control-plane | kube-system |
| kube-controller-manager-lc4j-refresh-control-plane | kube-system |
| kube-proxy-t7c85 | kube-system |
| kube-scheduler-lc4j-refresh-control-plane | kube-system |
| local-path-provisioner-7dc846544d-664q6 | local-path-storage |The new Pod shows up in the list, but not because the model remembered it: the assistant has no memory, and each chat call starts a new conversation.
Add a ChatMemory to the AI service if you want the model to remember the previous messages.
Small models need checking.
With the logEvents(true) option, you can see each tools/call request: in my runs, Qwen3 4B always called the right tool, but in one of them it listed the whole cluster with a status.phase=Running field selector I hadn't asked for, which would have hidden any pending or failing Pod.
With llama3.1:8b, a similar request failed: asked for the Pods in the default namespace, the model called pods_list with made-up arguments and answered with a tool call in plain text instead of the table.
I wrote more about this in Giving Superpowers to Small Language Models with MCP.
Limiting the tools the model can use
McpToolProvider hands the model every tool the server exposes, including destructive ones like pods_delete and resources_delete.
The simplest way to keep a model away from them is on the server side: the Kubernetes MCP Server's --read-only flag exposes only the tools annotated as read-only.
Read-only prevents changes, not reads: tools like resources_get can still return Secrets, which then reach the model (and its provider, if you use a hosted one); the server's denied_resources setting can keep them out.
initStdioClient("npx", "-y", "kubernetes-mcp-server@latest", "--read-only")With this flag, the client gets 14 tools instead of 20.
On the client side, McpToolProvider can filter the tools by name with filterToolNames("pods_list", "pods_get"), or with any predicate.
LangChain4j exposes each tool's MCP tool annotations in its metadata, so this filter keeps the same 14 read-only tools:
import dev.langchain4j.mcp.client.McpToolMetadataKeys;
McpToolProvider.builder()
.mcpClients(client)
.filter((mcpClient, tool) -> Boolean.TRUE.equals(tool.metadata().get(McpToolMetadataKeys.READ_ONLY_HINT)))
.build();Annotations are hints from the server, though: only filter on them for servers you trust.
Testing MCP servers with LangChain4j
Beyond everyday use cases, I also use this setup for testing MCP server implementations in Java, like the Kubernetes MCP server I wrote for the Quarkus MCP Servers project. By configuring an MCP client that connects to the server, I can simulate real-world interactions and verify the MCP server's behavior.
The tests in that repository still pin LangChain4j 1.0.0-beta2 and the SSE transport as of October 2026. The snippets below show the same setup with the current LangChain4j API.
I use these techniques with three levels of integration testing:
1. Unit testing
For testing individual units of logic, in Quarkus applications, you can start a client with the following code:
@QuarkusTest
class MCPServerKubernetesTest {
@TestHTTPResource
URL url;
private McpClient mcpClient;
@BeforeEach
void setUpMcpClient() {
mcpClient = DefaultMcpClient.builder()
.clientName("test-mcp-client-kubernetes")
.toolExecutionTimeout(Duration.ofSeconds(10))
.transport(StreamableHttpMcpTransport.builder().url(url.toString() + "mcp").build())
.build();
}
}Since Quarkus provides the application URL as a test resource, it's easy to set up an MCP client for testing.
The Quarkus MCP Server HTTP extension serves the Streamable HTTP endpoint at /mcp by default.
By combining the MCP client with a Kubernetes Mock Server like the one provided by the Fabric8 Java Kubernetes Client, you can then perform individual requests using the client and verify the behavior of specific units of logic.
final var result = mcpClient.executeTool(
ToolExecutionRequest.builder().name("name_of_the_tool_in_test").arguments("{}").build());
assertThat(result.resultText()).contains("expected output");2. Integration testing
Implementing larger integration tests for your application is also straightforward with LangChain4j and Quarkus. Considering a built application, you can start the real MCP server as a subprocess and connect to it using the STDIO transport method.
In the following code snippet, I show you how to achieve this in Quarkus:
public class MCPServerKubernetesIT {
private static McpClient mcpClient;
@BeforeAll
static void setUpMcpClient() throws Exception {
mcpClient = DefaultMcpClient.builder()
.clientName("test-mcp-client-kubernetes")
.toolExecutionTimeout(Duration.ofSeconds(10))
.transport(StdioMcpTransport.builder().command(
List.of(
// Retrieve the java binary location:
ProcessHandle.current().info().command().orElseThrow(),
"-jar",
// Location of the built application (provided by Quarkus):
System.getProperty("java.jar.path")
)).logEvents(true).build())
.build();
}
}In this example, we set up an MCP client using the STDIO transport method to connect to the packaged Quarkus application. This instructs the MCP client to spawn a real subprocess with the compiled application and communicate with it through standard input/output streams.
Just like in the unit testing example, you can then execute specific tools on the server and verify the behavior of the application as a whole.
3. End-to-end testing
For end-to-end testing, you can use the same setup as in integration testing but connect it to a real model instead. By following the same setup I described in the Example: a Java AI agent that uses the Kubernetes MCP Server section, you can create a full end-to-end test that simulates real-world interactions with your application.
By automating chat queries and comparing the responses against expected outputs, you can check that your MCP server's tools are described well enough for a model to use them.
However, note the non-deterministic nature of AI models, which may lead to different responses for the same input.
You should consider this when writing your end-to-end tests and allow for some flexibility in the expected outputs.
Asserting on the tool calls the model made is more reliable than asserting on its prose: declare the AI service method as Result<String> chat(String userMessage), and result.toolExecutions() lists every tool call with its arguments and result.
Can LangChain4j also build MCP servers?
The langchain4j-mcp module is a client only.
As of October 2026, the LangChain4j community project publishes a langchain4j-community-mcp-server module that exposes your @Tool-annotated methods as an MCP STDIO server (Building a Java MCP stdio server).
For a server with Streamable HTTP, I use the Quarkus MCP Server extension from the testing examples above.
Conclusion
LangChain4j's MCP client lets Java developers give any LangChain4j AI service the tools of an MCP server. Whether you're using a third-party MCP server or developing your own, the client takes a transport, a few optional settings, and a tool provider. By connecting your Java applications to MCP servers and Large Language Models (LLMs), you can create AI assistants that perform complex tasks and interact with external tools and services.
In this post, I've shown you how to set up an MCP client with LangChain4j over STDIO and Streamable HTTP, which protocol version to pick for each, and how to integrate it with a local model. I've also provided some insights on how to use LangChain4j to test your MCP server implementations, from unit tests to end-to-end tests.
The same MCP servers work from JavaScript too, with LangChain.js or the AI SDK.
You can find the source code for this post on GitHub.
You Might Also Like
- Introduction to the Model Context Protocol (MCP): The Future of AI Integration
- LangChain.js MCP Adapters: Connect Agents to MCP Servers
- AI SDK MCP Client: Connect JavaScript to an MCP Server
- Giving Superpowers to Small Language Models with Model Context Protocol (MCP)
- MCP Tool Annotations: Every Hint and Its Default Value
