Spring AI MCP Client And Server To Use Local LLM

MCP Client Server Application With Local LLM
With the AI being buzz word in the IT industry recently came across the work Model Context Protocol (MCP) and wanted to learn and understand about the protocol. This blog doesnβt details much about the MCP, please refer the MCP documentation. This would like a quick start with MCP using Spring AI and captures my learnings of using different tools.
Pre-requisites:
Docker Desktop
Java
Any IDE for Java development
What is MCP?
Model Context Protocol (MCP) is an open standard developed by Anthropic. MCP is an open protocol that standardizes how applications provide context to LLMs. MCP helps you build agents and complex workflows on top of LLMs. MCP standardize how AI applications, particularly large language models (LLMs), access and utilize external tools, data, and resources. For more details refer MCP documentation.
Sample Application
The basic idea is to use natural language to manage the Item list using the functionality created on the MCP server. The MCP Client will use the LLM running in local to infer the context and call the Tools to render the response.
Overall flow
The MCP Server and Client built with Spring AI uses STDIO transport for communication since it is simple to start with. MCP supports different transport like STDIO, SSE (Server-Sent Event).
STDIO
- Standard input and output (stdio) is the simplest and most universal transport for MCPSSE
- Server-sent events (SSE)- provide a persistent connection for server-to-client streaming, while client-to-server messages are sent using HTTP POST.
The MCP server code includes a service layer which has bunch of functionality to manage an in-memory Item list. The methods in the service layer are annotated with @Tools
. For Spring AI this annotation is imported from spring-ai-starter-mcp-server
dependency. The tools annotation includes name
and description
field. With the description
LLM will be able to set context and understand the methods functionality.
Tools
- Tools are a powerful primitive in the Model Context Protocol (MCP) that enable servers to expose executable functionality to clients. Through tools, LLMs can interact with external systems, perform computations, and take actions in the real world.
Running Ollama LLM in local docker container
- To run the Ollama container in local docker use below command
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
- Once Ollama docker container starts running in detached mode, we need to execute below command to start the llama3.2 model. The command will download the model. Note the volume in the above docker command -v is where the model will saved in local storage. Below command will run the llama3.2 model and stops in a prompt. Type
/bye
to quit the prompt.
docker exec -it ollama ollama run llama3.2
The MCP server include bunch of methods to manage the in-memory item list in the service layer. The Item is defined as Java record, with id, name and quantity fields. There service layer methods will list all the items from in-memory list, add and find item by name.
Note when testing with the natural language to add a item to the in-memory list there was an exception in the client this was due to text to json conversion where when the method was invoked the argument expected is to be Item. Refer the Output section below.
For initial code generation have used the start.spring.io
, and included only MCP server
dependency.
Server code details
pom.xml
The pom.xml also includes maven plugin to copy the generated jar to a different path. This is optional, I just wanted to keep the server jar that will be executed by the client mcp-server configuration in different folder. In the MCP Client code application.yaml we could see the classpath reference to the mcp-servers-config.json which includes the java command to start the server. This is done as we are using STDIO transport
<!-- file name: pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mcp.demo</groupId>
<artifactId>mcp-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp-server</name>
<description>Sample project MCP server</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>24</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<id>copy</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
</execution>
</executions>
<configuration>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<type>${project.packaging}</type>
</artifactItem>
</artifactItems>
<outputDirectory>C:\\AI-mcp\\jar</outputDirectory>
</configuration>
</plugin>
</plugins>
</build>
</project>
- The
resources/application.yaml
looks like below. Theloggging.pattern.console
is set to empty since when the MCP Client communicates to the MCP Server with STDIO transport we don't wan't the log message being logged in the console.
Note:
With the
logging.patter.console
empty, when starting the MCP server application in the IDE, you might see below message. This is fine the actual application is running as expected.
ERROR in ch.qos.logback.classic.PatternLayout("") - Empty or null pattern.
# file-name: resources/application.yaml
spring:
application:
name: item-mcp-server
main:
web-application-type: none
banner-mode: off
ai:
mcp:
server:
name: item-mcp-server
version: 1.0.0
# This is required for STDIO since the MCP Client will use java command to run the server
# Client expects STDIO to only print the server response
logging:
pattern:
console:
- The Item record looks like below, we use AtomicInteger to increment the Id field
package com.mcp.demo.server.data;
import java.util.concurrent.atomic.AtomicInteger;
public record Item(String name, int quantity, int id) {
private static final AtomicInteger counter = new AtomicInteger(0);
public Item {
if (id ==0 ){
id = counter.incrementAndGet();
}
}
public Item(String name, int quantity){
this(name,quantity,counter.incrementAndGet());
}
}
The Core service code that manages the Items in in-memory list
Refer the @Tools annotation in the service layer, the description includes information about the methods functionality. If the description is detailed enough with example, the LLM could better associate with the context.
package com.mcp.demo.server.service;
import com.mcp.demo.server.data.Item;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class ItemService {
public static final Logger log = LoggerFactory.getLogger(ItemService.class);
public List<Item> items = new ArrayList<>();
@Tool(name="t_get_all_items",description = "This method will get all the items stored in-memory list in this application")
public List<Item> getItems (){
return items;
}
@Tool(name="t_get_item_by_name",description = "This method will fetch one item based on the input name from the in-memory item lists, the name shouldn't be null")
public Item getItem(String name) throws IllegalArgumentException {
if(name == null || name.isEmpty()){
log.error("Name can't be empty");
throw new IllegalArgumentException("Name can't be empty for this request - t_get_item_by_name service");
}
return items.stream()
.filter(car -> car.name().equals(name))
.findFirst()
.orElse(null);
}
@Tool(name="t_add_item_to_list",description = "This method will add a single item to the in-memory list. " +
"The inputItem argument in the method should be an Item object which includes name and quantity field." +
"Before accessing this tool functionality from the client create the Item object with name and quantity" +
"and this function will add it to the in-memory list")
public String addItem(Item inputItem) throws IllegalArgumentException{
if(inputItem == null || inputItem.name() == null || inputItem.name().isEmpty()){
log.error("input Item name can't be empty");
throw new IllegalArgumentException("Input Item name can't be empty");
}
items.add(inputItem);
return inputItem.toString();
}
@PostConstruct
public void init(){
List<Item> carList = List.of(
new Item("Table",156),
new Item("Chair",510),
new Item("Cups",500),
new Item("Bottle",43),
new Item("Box",600)
);
items.addAll(carList);
}
}
- Below code shows how to register the item service with tools functionality to the ToolCallBackProvider. The MCP Client would be able to list the tools in the client side.
package com.mcp.demo.server.config;
import com.mcp.demo.server.service.ItemService;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class McpServerToolConfig {
@Bean
public ToolCallbackProvider toolCallbackProvider(ItemService itemService) {
return MethodToolCallbackProvider
.builder()
.toolObjects(itemService)
.build();
}
}
- Entry point of the server application, this is typical code generated by the spring starter io.
package com.mcp.demo.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
Testing the Server code
There are different options to test the MCP server code.
Like using Postman with MCP, Claude, Cursor, etc. In here have details testing using MCP Inspector which is from Anthropic community. To run the MCP Inspector we need node js to be installed in local machine.
Make sure to generate the server Jar, before testing with the tool, use
mvn clean install
to generate the jar. Jar will be copied to the path specified in the pom.xml maven plugin.
- To start the application we can use below command
npx @modelcontextprotocol/inspector
- The output of the command looks like below
Need to install the following packages:
@modelcontextprotocol/inspector@0.16.1
Ok to proceed? (y) y
Starting MCP inspector...
βοΈ Proxy server listening on localhost:6277
π Session token: 85b6c70dfebf439a3737efe462470fadad8f790bcc0c3d0ac56e2ffe99f07552
Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth
π MCP Inspector is up and running at:
http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=85b6c70dfebf439a3737efe462470fadad8f790bcc0c3d0ac56e2ffe99f07552
π Opening browser...
The browser will look like below to start with.
In the UI, we could select and update below configuration
Note:
Java should be accessible and update the UI with below command and arguments info
Transport Type: STDIO
Command: java
Arguments "-Dspring.ai.mcp.server.stdio=true" "-jar" "C:\\AI-mcp\\jar\\mcp-server-0.0.1-SNAPSHOT.jar"
Click the Connect button which should display the screen as seen below
Select the Tools tab and click the list Tools, which would list the service tool list
Using Claude desktop to test the server
Claude
desktop to connect to the server
- The Claude config looks like below, the server configuration below it also includes a filesystem mcp server which is optional.
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"C:\\thiru\\edu\\AI-mcp\\data"
]
},
"item-mcp": {
"command": "java",
"args": [
"-jar",
"C:\\thiru\\edu\\AI-mcp\\jar\\mcp-server-0.0.1-SNAPSHOT.jar"
]
}
}
}
From Claude, when we request to list all the items from the in-memory list, it will prompt to allow access, which looks like below
The response will look like below
MCP Client Code details
The MCP Client code is also generated using Spring Starter io and with the MCP Client dependency.
The Client code includes below dependencies
The MCP Client java dependency
spring-ai-starter-mcp-client
Ollama spring dependency
spring-ai-starter-model-ollama
spring-boot-starter-web
dependency expose an POST endpoint to send message using cURL
<!-- file-name: pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.mcp.demo</groupId>
<artifactId>mcp-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mcp-client</name>
<description>Sample project MCP server</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>24</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>
- Below is the InputController code, we use SyncMcpToolCallbackProvider,
Note:
Spring AI MCP Client autowires required beans, would recommend to refer the spring documentation
package com.mcp.demo.client.controller;
import io.modelcontextprotocol.client.McpSyncClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/input")
public class InputController {
private static final Logger log = LoggerFactory.getLogger(InputController.class);
private final ChatClient chatClient;
private final List<McpSyncClient> mcpSyncClients;
public InputController(ChatClient.Builder chatClientBuilder,
ToolCallbackProvider toolCallbackProvider,
List<McpSyncClient> mcpSyncClients){
this.chatClient = chatClientBuilder.build();
this.mcpSyncClients = mcpSyncClients;
// This prints the tools list mostly used for debugging
// This is optional
printToolInfoFromServer(toolCallbackProvider);
}
@PostMapping("/in")
public String input(@RequestBody String inputData){
log.info("input data received - {}",inputData);
return chatClient.prompt()
.user(inputData)
.toolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients))
.call()
.content();
}
private static void printToolInfoFromServer(ToolCallbackProvider toolCallbackProvider) {
List<ToolCallback> toolCallbacks = List.of(toolCallbackProvider.getToolCallbacks());
if(toolCallbacks.isEmpty()){
log.warn("No tools found");
} else {
System.out.println("**************************************");
for (ToolCallback toolCallback : toolCallbacks){
ToolDefinition toolDefinition = toolCallback.getToolDefinition();
System.out.println("Tool Name: "+toolDefinition.name());
System.out.println(" |___ Description: "+toolDefinition.description());
System.out.println(" |___ Input Schem: "+toolDefinition.inputSchema());
System.out.println("__________________________________");
}
System.out.println("**************************************");
}
}
}
- The
resources/application.yaml
configuration looks below
spring:
application:
name: item-mcp-client
main:
banner-mode: off
ai:
mcp:
client:
enabled: true
name: item-mcp-client
version: 1.0.0
toolcallback:
enabled: true
stdio:
# mcp servers config which includes the java command to start the server
servers-configuration: classpath:mcp-servers-config.json
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3.2
logging:
level:
io:
modelcontextprotocol:
client: trace
spec: trace
- The
mcp-servers-config.json
, this includes the command to start the server jar
{
"mcpServers": {
"item-mcp-client": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-jar",
"C:\\thiru\\edu\\AI-mcp\\jar\\mcp-server-0.0.1-SNAPSHOT.jar"
]
}
}
}
Output:
- With the MCP client and MCP server running, below are sample outputs that was generated for testing using cURL command
$ curl http://localhost:8080/input/in -d'List all the items from the server' -H 'Content-Type: application/json'
Here is the list of items from the server:
* Table: 156 items, ID: 1
* Chair: 510 items, ID: 2
* Cups: 500 items, ID: 3
* Bottle: 43 items, ID: 4
* Box: 600 items, ID: 5
Note:
When tried to add an item to the list with just simple instruction noticed error message in the MCP client like below. This is because the LLM is not able to infer and create the Item object before invoking the specific tools functionality.
java.lang.IllegalStateException: Error calling tool: [TextContent[audience=null, priority=null, text=Conversion from JSON to com.mcp.demo.server.data.Item failed]]
$ curl http://localhost:8080/input/in -d'Please add an item with name Calculator and quantity 5 to the existing in-memory list' -H 'Content-Type: application/json'
{"timestamp":"2025-07-12T21:42:54.166+00:00","status":500,"error":"Internal Server Error","path":"/input/in"}
- Sending more specific instruction to the LLM to add the item
$ curl http://localhost:8080/input/in -d'Please add an item with name Calculator and quantity 5 to the existing in-memory list, make sure to invoke the tools functionality with Item object with name and quantity field.' -H 'Content-Type: application/json'
The tool has added an item to the list with name 'Calculator' and quantity 5. The new item's ID is 6. Here's a formatted answer:
The item "Calculator" with quantity 5 has been successfully added to the list. The item's details are as follows:
Name: Calculator
Quantity: 5
ID: 6
$ curl http://localhost:8080/input/in -d'List all the items from the server' -H 'Content-Type: application/json'
Here is the list of items from the server:
1. Table - 156 units
2. Chair - 510 units
3. Cups - 500 units
4. Bottle - 43 units
5. Box - 600 units
6. Calculator - 5 units
Reference:
MCP Client and Server code in Github
Subscribe to my newsletter
Read articles from Thirumurthi S directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
