Using Model Context Protocol (MCP) with Spring AI

This article will show how to use Spring AI support for MCP (Model Context Protocol) in Spring Boot server-side and client-side applications. You will learn how to serve tools and prompts on the server side and discover them on the client-side Spring AI application. The Model Context Protocol is a standard for managing contextual interactions with AI models. It provides a standardized way to connect AI models to external data sources and tools. It can help with building complex workflows on top of LLMs. Spring AI MCP extends the MCP Java SDK and provides client and server Spring Boot starters. The MCP Client is responsible for establishing and managing connections with MCP servers.
This is the seventh part of my series of articles about Spring Boot and AI. It is worth reading the following posts before proceeding with the current one. Please pay special attention to the last article from the list about the tool calling feature since we will implement it in our sample client and server apps using MCP.
- https://piotrminkowski.com/2025/01/28/getting-started-with-spring-ai-and-chat-model: The first tutorial introduces the Spring AI project and its support for building applications based on chat models like OpenAI or Mistral AI.
- https://piotrminkowski.com/2025/01/30/getting-started-with-spring-ai-function-calling: The second tutorial shows Spring AI support for Java function calling with the OpenAI chat model.
- https://piotrminkowski.com/2025/02/24/using-rag-and-vector-store-with-spring-ai: The third tutorial shows Spring AI support for RAG (Retrieval Augmented Generation) and vector store.
- https://piotrminkowski.com/2025/03/04/spring-ai-with-multimodality-and-images: The fourth tutorial shows Spring AI support for a multimodality feature and image generation
- https://piotrminkowski.com/2025/03/10/using-ollama-with-spring-ai: The fifth tutorial shows Spring AI support for interactions with AI models run with Ollama
- https://piotrminkowski.com/2025/03/13/tool-calling-with-spring-ai: The sixth tutorial show Spring AI for the Tool Calling feature.
Source Code
Feel free to use my source code if you’d like to try it out yourself. To do that, you must clone my sample GitHub repository. Then you should only follow my instructions.
Motivation for MCP with Spring AI
MCP introduces an interesting concept for applications interacting with AI models. With MCP the application can provide specific tools/functions for several other services, which need to use data exposed by that application. Additionally, it can expose prompt templates and resources. Thanks to that, we don’t need to implement AI tools/functions inside every client service but integrate them with the application that exposes tools over MCP.
The best way to analyze the MCP concept is through an example. Let’s consider an application that connects to a database and exposes data through REST endpoints. If we want to use that data in our AI application we should implement and register AI tools that retrieve data by connecting such the REST endpoints. So, each client-side application that needs data from the source service would have to implement its own set of AI tools locally. Here comes the MCP concept. The source service defines and exposes AI tools/functions in the standardized form. All other apps that need to provide data to AI models can load and use a predefined set of tools.
The following diagram illustrates our scenario. Two Spring Boot applications act as MCP servers. They connect to the database and use Spring AI MCP Server support to expose @Tool
methods to the MCP client-side app. The client-side app communicates with the OpenAI model. It includes the tools exposed by the server-side apps in the user query to the AI model. The person-mcp-service
app provides @Tool
methods for searching persons in the database table. The account-mcp-service
is doing the same for the persons’ accounts.

Build MCP Server App with Spring AI
Let’s begin with the implementation of applications that act as MCP servers. They both run and use an in-memory H2 database. To interact with a database we include the Spring Data JPA module. Spring AI allows us to switch between three transport types: STDIO, Spring MVC, and Spring WebFlux. MCP Server with Spring WebFlux supports Server-Sent Events (SSE) and an optional STDIO
transport. Here’s a list of required Maven dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-webflux-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
XMLCreate the Person MCP Server
Here’s an @Entity
class for interacting with the person
table:
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private int age;
private String nationality;
@Enumerated(EnumType.STRING)
private Gender gender;
// ... getters and setters
}
JavaThe Spring Data Repository interface contains a single method for searching persons by their nationality:
public interface PersonRepository extends CrudRepository<Person, Long> {
List<Person> findByNationality(String nationality);
}
JavaThe PersonTools
@Service
bean contains two Spring AI @Tool
methods. It injects the PersonRepository
bean to interact with the H2 database. The getPersonById
method returns a single person with a specific ID field, while the getPersonsByNationality
returns a list of all persons with a given nationality.
@Service
public class PersonTools {
private PersonRepository personRepository;
public PersonTools(PersonRepository personRepository) {
this.personRepository = personRepository;
}
@Tool(description = "Find person by ID")
public Person getPersonById(
@ToolParam(description = "Person ID") Long id) {
return personRepository.findById(id).orElse(null);
}
@Tool(description = "Find all persons by nationality")
public List<Person> getPersonsByNationality(
@ToolParam(description = "Nationality") String nationality) {
return personRepository.findByNationality(nationality);
}
}
JavaOnce we define @Tool
methods, we must register them within the Spring AI MCP server. We can use the ToolCallbackProvider
bean for that. More specifically, the MethodToolCallbackProvider
class provides a builder that creates an instance of the ToolCallbackProvider
class with a list of references to objects with @Tool
methods.
@SpringBootApplication
public class PersonMCPServer {
public static void main(String[] args) {
SpringApplication.run(PersonMCPServer.class, args);
}
@Bean
public ToolCallbackProvider tools(PersonTools personTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(personTools)
.build();
}
}
JavaFinally, we must provide configuration properties. The person-mcp-server
app will listen on the 8060
port. We should also set the name and version of the MCP server embedded in our application.
spring:
ai:
mcp:
server:
name: person-mcp-server
version: 1.0.0
jpa:
database-platform: H2
generate-ddl: true
hibernate:
ddl-auto: create-drop
logging.level.org.springframework.ai: DEBUG
server.port: 8060
YAMLThat’s all. We can start the application.
$ cd spring-ai-mcp/person-mcp-service
$ mvn spring-boot:run
ShellSessionCreate the Account MCP Server
Then, we will do very similar things in the second application that acts as an MCP server. Here’s the @Entity
class for interacting with the account
table:
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String number;
private int balance;
private Long personId;
// ... getters and setters
}
JavaThe Spring Data Repository interface contains a single method for searching accounts belonging to a given person:
public interface AccountRepository extends CrudRepository<Account, Long> {
List<Account> findByPersonId(Long personId);
}
JavaThe AccountTools
@Service
bean contains a single Spring AI @Tool
method. It injects the AccountRepository
bean to interact with the H2 database. The getAccountsByPersonId
method returns a list of accounts owned by the person with a specified ID field value.
@Service
public class AccountTools {
private AccountRepository accountRepository;
public AccountTools(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
@Tool(description = "Find all accounts by person ID")
public List<Account> getAccountsByPersonId(
@ToolParam(description = "Person ID") Long personId) {
return accountRepository.findByPersonId(personId);
}
}
JavaOf course, the account-mcp-server
application will use ToolCallbackProvider
to register @Tool
methods defined inside the AccountTools
class.
@SpringBootApplication
public class AccountMCPService {
public static void main(String[] args) {
SpringApplication.run(AccountMCPService.class, args);
}
@Bean
public ToolCallbackProvider tools(AccountTools accountTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(accountTools)
.build();
}
}
JavaHere are the application configuration properties. The account-mcp-server
app will listen on the 8040
port.
spring:
ai:
mcp:
server:
name: account-mcp-server
version: 1.0.0
jpa:
database-platform: H2
generate-ddl: true
hibernate:
ddl-auto: create-drop
logging.level.org.springframework.ai: DEBUG
server.port: 8040
YAMLLet’s run the second server-side app:
$ cd spring-ai-mcp/account-mcp-service
$ mvn spring-boot:run
ShellSessionOnce we start the application, we should see the log indicating how many tools were registered in the MCP server.

Build MCP Client App with Spring AI
Implementation
We will create a single client-side application. However, we can imagine an architecture where many applications consume tools exposed by one MCP server. Our application interacts with the OpenAI chat model, so we must include the Spring AI OpenAI starter. For the MCP Client starter, we can choose between two dependencies: Standard MCP client and Spring WebFlux client. Spring team recommends using the WebFlux-based SSE connection with the spring-ai-mcp-client-webflux-spring-boot-starter
. Finally, we include the Spring Web starter to expose the REST endpoint. However, you can use Spring WebFlux starter to expose them reactively.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-webflux-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
XMLOur MCP client connects with two MCP servers. We must provide the following connection settings in the application.yml
file.
spring.ai.mcp.client.sse.connections:
person-mcp-server:
url: http://localhost:8060
account-mcp-server:
url: http://localhost:8040
ShellSessionOur sample Spring Boot application contains to @RestController
s, which expose HTTP endpoints. The PersonController
class defines two endpoints for searching and counting persons by nationality. The MCP Client Boot Starter automatically configures tool callbacks that integrate with Spring AI’s tool execution framework. Thanks to that we can use the ToolCallbackProvider
instance to provide default tools to the ChatClient
bean. Then, we can perform the standard steps to interact with the AI model with Spring AI ChatClient
. However, the client will use tools exposed by both sample MCP servers.
@RestController
@RequestMapping("/persons")
public class PersonController {
private final static Logger LOG = LoggerFactory
.getLogger(PersonController.class);
private final ChatClient chatClient;
public PersonController(ChatClient.Builder chatClientBuilder,
ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
.defaultTools(tools)
.build();
}
@GetMapping("/nationality/{nationality}")
String findByNationality(@PathVariable String nationality) {
PromptTemplate pt = new PromptTemplate("""
Find persons with {nationality} nationality.
""");
Prompt p = pt.create(Map.of("nationality", nationality));
return this.chatClient.prompt(p)
.call()
.content();
}
@GetMapping("/count-by-nationality/{nationality}")
String countByNationality(@PathVariable String nationality) {
PromptTemplate pt = new PromptTemplate("""
How many persons come from {nationality} ?
""");
Prompt p = pt.create(Map.of("nationality", nationality));
return this.chatClient.prompt(p)
.call()
.content();
}
}
JavaLet’s switch to the second @RestController
. The AccountController
class defines two endpoints for searching accounts by person ID. The GET /accounts/count-by-person-id/{personId}
returns the number of accounts belonging to a given person. The GET /accounts/balance-by-person-id/{personId}
is slightly more complex. It counts the total balance in all person’s accounts. However, it must also return the person’s name and nationality, which means that it must call the getPersonById
tool method exposed by the person-mcp-server
app after calling the tool for searching accounts by person ID.
@RestController
@RequestMapping("/accounts")
public class AccountController {
private final static Logger LOG = LoggerFactory.getLogger(PersonController.class);
private final ChatClient chatClient;
public AccountController(ChatClient.Builder chatClientBuilder,
ToolCallbackProvider tools) {
this.chatClient = chatClientBuilder
.defaultTools(tools)
.build();
}
@GetMapping("/count-by-person-id/{personId}")
String countByPersonId(@PathVariable String personId) {
PromptTemplate pt = new PromptTemplate("""
How many accounts has person with {personId} ID ?
""");
Prompt p = pt.create(Map.of("personId", personId));
return this.chatClient.prompt(p)
.call()
.content();
}
@GetMapping("/balance-by-person-id/{personId}")
String balanceByPersonId(@PathVariable String personId) {
PromptTemplate pt = new PromptTemplate("""
How many accounts has person with {personId} ID ?
Return person name, nationality and a total balance on his/her accounts.
""");
Prompt p = pt.create(Map.of("personId", personId));
return this.chatClient.prompt(p)
.call()
.content();
}
}
JavaRunning the Application
Before starting the client-side app we must export the OpenAI token as the SPRING_AI_OPENAI_API_KEY
environment variable.
export SPRING_AI_OPENAI_API_KEY=<YOUR_OPENAI_API_KEY>
ShellSessionThen go to the sample-client
directory and run the app with the following command:
$ cd spring-ai-mcp/sample-client
$ mvn spring-boot:run
ShellSessionOnce we start the application, we can switch to the logs. As you see, the sample-client
app receives responses with tools from both person-mcp-server
and account-mcp-server
apps.

Testing MCP with Spring Boot
Both server-side applications load data from the import.sql
scripts on startup. Spring Data JPA automatically imports data from such scripts. Our MCP client application listens on the 8080
port. Let’s call the first endpoint to get a list of persons from Germany:
curl http://localhost:8080/persons/nationality/Germany
ShellSessionHere’s the response from the OpenAI model:

We can also call the endpoint that counts the number with a given nationality.
curl http://localhost:8080/persons/count-by-nationality/Germany
ShellSessionAs the final test, we can call the GET /accounts/balance-by-person-id/{personId}
endpoint that interacts with tools exposed by both MCP server-side apps. It requires an AI model to combine data from person and account sources.

Exposing Prompts with MCP
We can also expose prompts and resources with the Spring AI MCP server support. To register and expose prompts we need to define the list of SyncPromptRegistration
objects. It contains the name of the prompt, a list of input arguments, and a text content.
@SpringBootApplication
public class PersonMCPServer {
public static void main(String[] args) {
SpringApplication.run(PersonMCPServer.class, args);
}
@Bean
public ToolCallbackProvider tools(PersonTools personTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(personTools)
.build();
}
@Bean
public List<McpServerFeatures.SyncPromptRegistration> prompts() {
var prompt = new McpSchema.Prompt("persons-by-nationality", "Get persons by nationality",
List.of(new McpSchema.PromptArgument("nationality", "Person nationality", true)));
var promptRegistration = new McpServerFeatures.SyncPromptRegistration(prompt, getPromptRequest -> {
String argument = (String) getPromptRequest.arguments().get("nationality");
var userMessage = new McpSchema.PromptMessage(McpSchema.Role.USER,
new McpSchema.TextContent("How many persons come from " + argument + " ?"));
return new McpSchema.GetPromptResult("Count persons by nationality", List.of(userMessage));
});
return List.of(promptRegistration);
}
}
ShellSessionAfter startup, the application prints information about a list of registered prompts in the logs.

There is no built-in Spring AI support for loading prompts using the MCP client. However, Spring AI MCP support is under active development so we may expect some new features soon. For now, Spring AI provides the auto-configured instance of McpSyncClient
. We can use it to search the prompt in the list of prompts received from the server. Then, we can prepare the PromptTemplate
instance using the registered content and create the Prompt
by filling the template with the input parameters.
@RestController
@RequestMapping("/persons")
public class PersonController {
private final static Logger LOG = LoggerFactory
.getLogger(PersonController.class);
private final ChatClient chatClient;
private final List<McpSyncClient> mcpSyncClients;
public PersonController(ChatClient.Builder chatClientBuilder,
ToolCallbackProvider tools,
List<McpSyncClient> mcpSyncClients) {
this.chatClient = chatClientBuilder
.defaultTools(tools)
.build();
this.mcpSyncClients = mcpSyncClients;
}
// ... other endpoints
@GetMapping("/count-by-nationality-from-client/{nationality}")
String countByNationalityFromClient(@PathVariable String nationality) {
return this.chatClient
.prompt(loadPromptByName("persons-by-nationality", nationality))
.call()
.content();
}
Prompt loadPromptByName(String name, String nationality) {
McpSchema.GetPromptRequest r = new McpSchema
.GetPromptRequest(name, Map.of("nationality", nationality));
var client = mcpSyncClients.stream()
.filter(c -> c.getServerInfo().name().equals("person-mcp-server"))
.findFirst();
if (client.isPresent()) {
var content = (McpSchema.TextContent) client.get()
.getPrompt(r)
.messages()
.getFirst()
.content();
PromptTemplate pt = new PromptTemplate(content.text());
Prompt p = pt.create(Map.of("nationality", nationality));
LOG.info("Prompt: {}", p);
return p;
} else return null;
}
}
JavaFinal Thoughts
Model Context Protocol is an important initiative in the AI world. It allows us to avoid reinventing the wheel for each new data source. A unified protocol streamlines integration, minimizing development time and complexity. As businesses expand their AI toolsets, MCP enables seamless connectivity across multiple systems without the burden of excessive custom code. Spring AI introduced the initial version of MCP support recently. It seems promising. With Spring AI Client and Server starters, we may implement a distributed architecture, where several different apps use the AI tools exposed by a single service.
Leave a Reply