Chapter 3: Extend Your AI Agent with the Model Context Protocol (MCP)
At this point, our AI agent has become much more capable.
It can:
- Answer questions using a Large Language Model (LLM).
- Maintain conversation history using Chat Memory.
- Respond consistently based on its System Prompt.
However, it still has one important limitation.
It cannot interact with external systems.
For example, consider the following requests:
- Show today’s sales revenue.
- Find customer CUST-1001.
- Create a Jira ticket.
- Read the latest project documentation.
- Approve a leave request.
- Check inventory for Product A.
All of this information exists outside the LLM—in databases, REST APIs, SaaS applications, or enterprise systems.
So how does an AI agent access this information?
There are two approaches:
- Connect directly to APIs.
- Use the Model Context Protocol (MCP).
Many developers ask:
“Why do we need MCP? Can’t we simply call REST APIs?”
The answer is yes—you absolutely can build AI applications using REST APIs.
The question isn’t whether APIs work.
The question is which approach scales better for AI agents.
Option 1: Calling REST APIs Directly
Suppose our AI assistant needs to interact with three business systems.
- Employee Service
- Inventory Service
- Jira
Without MCP, the architecture might look like this.
AI Agent
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Employee API Inventory API Jira REST API
At first glance, this seems simple.
However, someone has to write all of the integration code.
For every API, developers must implement:
- HTTP client
- Authentication
- Authorization
- JSON serialization
- Error handling
- Retry logic
- Timeouts
- API version compatibility
For example, if your AI agent needs to retrieve employee information, your Java code might look like this:
Employee employee = employeeApiClient.findEmployee(employeeId);
return chatClient.prompt("""
Summarize the following employee.
%s
""".formatted(employee))
.call()
.content();
Notice something important.
Your application—not the LLM—is deciding:
- Which API to call.
- When to call it.
- Which parameters to pass.
- How to combine multiple API responses.
As the number of APIs grows, this orchestration logic becomes increasingly complex.
The Challenge with APIs
Now imagine your AI platform needs access to:
- Employee API
- Customer API
- Order API
- Inventory API
- Finance API
- Jira
- GitHub
- Slack
- Salesforce
- SAP
Your application gradually becomes an integration layer.
AI Agent
┌──────────────┼──────────────────┐
▼ ▼ ▼
Employee API Inventory API Customer API
▼ ▼ ▼
Finance API Jira API GitHub API
▼ ▼ ▼
Slack API Salesforce API SAP API
Each API has:
- Different authentication
- Different request formats
- Different response structures
- Different documentation
- Different SDKs
The AI agent itself isn’t becoming smarter.
Your application is simply accumulating more integration code.
Another Challenge: The LLM Doesn’t Understand Your APIs
Suppose your application exposes the following endpoint:
GET /api/v1/employees/{id}
The LLM doesn’t automatically know:
- That this endpoint exists.
- What it does.
- Which parameters it requires.
- When it should be invoked.
- What the response looks like.
Developers must manually write orchestration logic.
For example:
if(userPrompt.contains("employee")){
Employee employee =
employeeApi.findEmployee(id);
return summarize(employee);
}
Or they must build custom function-calling definitions for every API.
As the number of APIs increases, maintaining this mapping becomes more difficult.
This is exactly the problem that Model Context Protocol (MCP) was designed to solve.
Enter MCP
MCP takes a different approach.
Instead of hardcoding every API integration, developers expose their APIs as discoverable tools with machine-readable schemas.
Using these schemas, the LLM can automatically understand:
- What tools are available
- What each tool does
- What inputs it requires
- When it should be invoked
- What output it returns
This eliminates the need to manually write orchestration logic or create custom function definitions for every API.
As new APIs are added, they simply become new tools that the AI agent can discover and use.
The result is a much more scalable architecture: the LLM focuses on reasoning and decision-making, while MCP handles tool discovery, capability descriptions, and standardized invocation. This makes it far easier to build and maintain enterprise AI applications with hundreds or even thousands of APIs.
A recommended structure is:
Part 1 – Employee MCP Server
- Complete
pom.xml application.ymlEmployee.javaEmployeeService.javaEmployeeTools.java- Main class
- Run and test
Part 2 – Inventory MCP Server
- Complete
pom.xml application.ymlInventory.javaInventoryService.javaInventoryTools.java- Main class
- Run and test
Part 3 – Spring AI Agent
- Complete
pom.xml application.ymlAIConfigAssistantAgentAgentControllerChatRequest- Main class
Part 4 – End-to-End Demo
- Start both MCP servers
- Start AI Agent
- Sample prompts
- Internal flow diagrams
- How Spring AI discovers tools
- Multiple tool invocation
- Best practices
Chapter 1 – Building the Employee MCP Server
Project Structure
employee-mcp-server
│
├── src
│ ├── main
│ │ ├── java
│ │ │
│ │ └── com.example.employee
│ │ ├── EmployeeMcpServerApplication.java
│ │ ├── config
│ │ ├── model
│ │ │ Employee.java
│ │ ├── service
│ │ │ EmployeeService.java
│ │ └── tools
│ │ EmployeeTools.java
│ │
│ └── resources
│ application.yml
│
└── pom.xml
Step 1: 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>
<groupId>com.example.ai</groupId>
<artifactId>employee-mcp-server</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.1</spring-ai.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
</project>
Step 2: application.yml
server:
port: 8081
spring:
application:
name: employee-mcp-server
ai:
mcp:
server:
name: employee-server
version: 1.0.0
type: SYNC
This configuration starts an MCP server named employee-server on port 8081.
Step 3: Employee.java
package com.example.employee.model;
public record Employee(
Long id,
String name,
String department,
String email,
String manager
) {
}
Step 4: EmployeeService.java
package com.example.employee.service;
import com.example.employee.model.Employee;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class EmployeeService {
private final List<Employee> employees = List.of(
new Employee(
1L,
"Alice Johnson",
"Engineering",
"alice@company.com",
"David Miller"
),
new Employee(
2L,
"John Smith",
"Finance",
"john@company.com",
"Sophia Brown"
),
new Employee(
3L,
"Emma Wilson",
"Human Resources",
"emma@company.com",
"Michael Davis"
)
);
public Employee findEmployee(String employeeName) {
return employees.stream()
.filter(employee ->
employee.name().equalsIgnoreCase(employeeName))
.findFirst()
.orElse(null);
}
public List<Employee> findAllEmployees() {
return employees;
}
}
Step 5: EmployeeTools.java
This class exposes the business functionality as MCP tools.
package com.example.employee.tools;
import com.example.employee.model.Employee;
import com.example.employee.service.EmployeeService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class EmployeeTools {
private final EmployeeService employeeService;
public EmployeeTools(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@Tool(description = "Find employee details by employee name")
public Employee findEmployee(String employeeName) {
return employeeService.findEmployee(employeeName);
}
@Tool(description = "List all employees")
public List<Employee> listEmployees() {
return employeeService.findAllEmployees();
}
}
Notice that there is no REST controller.
The methods annotated with @Tool are automatically exposed by the MCP server.
Step 6: Main Class
package com.example.employee;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class EmployeeMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(
EmployeeMcpServerApplication.class,
args
);
}
}
Step 7: Run the Server
Start the application.
You should see the Spring Boot application start successfully on port 8081.
At this point:
- The Employee MCP Server is running.
- The tools are automatically registered with the MCP server.
- Any MCP-compatible client can discover and invoke them.
No additional controller or endpoint implementation is required.
Chapter 2 – Building the Inventory MCP Server
In the previous chapter, we built our first MCP Server that exposes employee-related tools.
Now let’s build a second MCP Server for the Inventory domain.
This server will provide AI agents with real-time inventory information, such as:
- Find a product by name.
- Check available stock.
- Identify the warehouse where the product is stored.
- List all available products.
The Inventory MCP Server is a completely independent Spring Boot application. In a real enterprise, it could be owned and maintained by a different team, while AI agents discover and use its tools through the Model Context Protocol (MCP).
Project Structure
inventory-mcp-server
│
├── src
│ ├── main
│ │ ├── java
│ │ │
│ │ └── com.example.inventory
│ │ ├── InventoryMcpServerApplication.java
│ │ ├── model
│ │ │ Inventory.java
│ │ ├── service
│ │ │ InventoryService.java
│ │ └── tools
│ │ InventoryTools.java
│ │
│ └── resources
│ application.yml
│
└── pom.xml
Step 1: pom.xml
The Inventory MCP Server uses the same dependencies as the Employee MCP Server.
<?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>
<groupId>com.example.ai</groupId>
<artifactId>inventory-mcp-server</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.1</spring-ai.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
</dependencies>
</project>
Step 2: application.yml
Run the Inventory MCP Server on a different port.
server:
port: 8082
spring:
application:
name: inventory-mcp-server
ai:
mcp:
server:
name: inventory-server
version: 1.0.0
type: SYNC
Now we have two independent MCP servers:
| Server | Port |
|---|---|
| Employee MCP Server | 8081 |
| Inventory MCP Server | 8082 |
Step 3: Create the Inventory Model
package com.example.inventory.model;
public record Inventory(
String productCode,
String productName,
int availableQuantity,
String warehouse
) {
}
Step 4: Create the Inventory Service
For simplicity, we’ll use an in-memory list.
Later, you can replace this with a database or an ERP system.
package com.example.inventory.service;
import com.example.inventory.model.Inventory;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class InventoryService {
private final List<Inventory> inventory = List.of(
new Inventory(
"P100",
"MacBook Pro",
12,
"London Warehouse"
),
new Inventory(
"P200",
"Dell XPS 15",
8,
"Manchester Warehouse"
),
new Inventory(
"P300",
"Lenovo ThinkPad X1",
25,
"Birmingham Warehouse"
)
);
public Inventory findProduct(String productName) {
return inventory.stream()
.filter(product ->
product.productName()
.equalsIgnoreCase(productName))
.findFirst()
.orElse(null);
}
public List<Inventory> findAllProducts() {
return inventory;
}
}
Step 5: Expose Inventory as MCP Tools
This class exposes business capabilities to AI agents.
package com.example.inventory.tools;
import com.example.inventory.model.Inventory;
import com.example.inventory.service.InventoryService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class InventoryTools {
private final InventoryService inventoryService;
public InventoryTools(InventoryService inventoryService) {
this.inventoryService = inventoryService;
}
@Tool(description = "Find inventory details by product name")
public Inventory findInventory(String productName) {
return inventoryService.findProduct(productName);
}
@Tool(description = "List all available products")
public List<Inventory> listProducts() {
return inventoryService.findAllProducts();
}
}
Notice that the tools expose business capabilities, not REST endpoints.
The AI agent doesn’t need to know where the data comes from—it simply discovers and invokes the available tools.
Step 6: Create the Main Class
package com.example.inventory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class InventoryMcpServerApplication {
public static void main(String[] args) {
SpringApplication.run(
InventoryMcpServerApplication.class,
args
);
}
}
Step 7: Run the Inventory MCP Server
Start the application.
The Inventory MCP Server will be available on port 8082.
At this point, you should have two independent MCP servers running:
Employee MCP Server
Port : 8081
Inventory MCP Server
Port : 8082
Each server exposes its own set of tools.
The Employee MCP Server provides:
findEmployee()listEmployees()
The Inventory MCP Server provides:
findInventory()listProducts()
Neither server knows anything about the other. They are completely independent services that can evolve, scale, and be deployed separately.
Architecture So Far
Our platform now looks like this:
AI Agent
│
(To be built next)
│
┌──────────┴──────────┐
▼ ▼
Employee MCP Server Inventory MCP Server
│ │
Employee Service Inventory Service
At this stage, we have successfully built two standalone MCP servers. However, there is still no AI agent to consume them.
In the next chapter, we’ll build a Spring AI-powered AI Agent that acts as an MCP Client, discovers tools from both servers automatically, and invokes them based on the user’s natural language requests—without any custom routing or orchestration logic.
Chapter 3 – Building the AI Agent with Spring AI
In the previous chapters, we built two independent MCP servers:
- Employee MCP Server
- Inventory MCP Server
Both servers expose business capabilities using the Model Context Protocol (MCP).
Now it’s time to build the AI Agent that will connect to these servers.
Unlike the MCP servers, this application doesn’t contain any business logic. Its responsibilities are to:
- Accept user requests.
- Send prompts to the Large Language Model (LLM).
- Discover tools from connected MCP servers.
- Invoke the appropriate tools.
- Return a natural language response.
The AI Agent acts as an MCP Client.
Project Structure
ai-agent
│
├── src
│ ├── main
│ │ ├── java
│ │ │
│ │ └── com.example.agent
│ │ ├── AiAgentApplication.java
│ │ ├── config
│ │ │ AIConfig.java
│ │ ├── controller
│ │ │ AgentController.java
│ │ ├── model
│ │ │ ChatRequest.java
│ │ └── service
│ │ AssistantAgent.java
│ │
│ └── resources
│ application.yml
│
└── pom.xml
Step 1: pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.ai</groupId>
<artifactId>ai-agent</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
</parent>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.0.1</spring-ai.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-client-webmvc</artifactId>
</dependency>
</dependencies>
</project>
Step 2: application.yml
Configure the OpenAI model and both MCP server connections.
server:
port: 8080
spring:
application:
name: ai-agent
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4.1
mcp:
client:
enabled: true
sse:
connections:
employee-server:
url: http://localhost:8081
inventory-server:
url: http://localhost:8082
Notice that we only configure the URLs of the MCP servers.
There is no Java code for discovering tools.
Spring AI handles that automatically.
Step 3: Configure the ChatClient
package com.example.agent.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AIConfig {
@Bean
ChatClient chatClient(ChatModel chatModel,
ToolCallbackProvider toolCallbackProvider) {
return ChatClient.builder(chatModel)
.defaultSystem("""
You are an Enterprise AI Assistant.
Responsibilities:
- Answer user questions.
- Retrieve employee information.
- Check inventory.
- Use available MCP tools whenever needed.
- Never make up business data.
""")
.defaultToolCallbacks(toolCallbackProvider)
.build();
}
}
The important line is:
.defaultToolCallbacks(toolCallbackProvider)
Spring AI automatically discovers all tools exposed by the connected MCP servers and registers them with the ChatClient.
No manual registration is required.
Step 4: Create the AI Service
package com.example.agent.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class AssistantAgent {
private final ChatClient chatClient;
public AssistantAgent(ChatClient chatClient) {
this.chatClient = chatClient;
}
public String chat(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
Notice how simple the service is.
There is:
- no HTTP client,
- no API client,
- no tool selection logic,
- no orchestration code.
The LLM decides which MCP tools to invoke.
Step 5: Create the Request Model
package com.example.agent.model;
public record ChatRequest(
String message
) {
}
Step 6: Create the REST Controller
package com.example.agent.controller;
import com.example.agent.model.ChatRequest;
import com.example.agent.service.AssistantAgent;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/agent")
public class AgentController {
private final AssistantAgent assistantAgent;
public AgentController(AssistantAgent assistantAgent) {
this.assistantAgent = assistantAgent;
}
@PostMapping
public String chat(@RequestBody ChatRequest request) {
return assistantAgent.chat(
request.message()
);
}
}
Step 7: Create the Main Class
package com.example.agent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AiAgentApplication {
public static void main(String[] args) {
SpringApplication.run(
AiAgentApplication.class,
args
);
}
}
Running the Complete Solution
Start the applications in the following order:
- Employee MCP Server (Port 8081)
- Inventory MCP Server (Port 8082)
- AI Agent (Port 8080)
At startup, the AI Agent connects to both MCP servers and automatically discovers their available tools.
The architecture now looks like this:
User
│
▼
AI Agent (8080)
│
Spring AI MCP Client
│
┌─────────┴─────────┐
▼ ▼
Employee MCP Server Inventory MCP Server
(8081) (8082)
Testing the AI Agent
Example 1
Request
POST /agent
{
"message": "Who is Alice Johnson?"
}
The LLM determines that employee information is required and invokes the findEmployee tool on the Employee MCP Server.
Example response:
Alice Johnson works in the Engineering department.
Her manager is David Miller.
Her email address is alice@company.com.
Example 2
{
"message":"Do we have MacBook Pro in stock?"
}
The AI invokes the findInventory tool on the Inventory MCP Server.
Example response:
Yes.
MacBook Pro is currently available.
Available Quantity: 12
Warehouse: London Warehouse.
Example 3
{
"message":"Who is Alice Johnson, and do we have a MacBook Pro available for her team?"
}
This request requires information from both MCP servers.
Internally, the AI Agent performs two tool calls:
findEmployee("Alice Johnson")findInventory("MacBook Pro")
The LLM combines the results into a single, conversational response.
What We’ve Built
In this chapter, we’ve completed the AI side of our MCP architecture.
Our AI Agent:
- Connects to multiple MCP servers.
- Automatically discovers available tools.
- Allows the LLM to decide which tools to invoke.
- Returns natural language responses based on live business data.
The application contains no custom integration logic, no API orchestration, and no manual tool routing. Spring AI and MCP handle these responsibilities, allowing you to focus on building business capabilities rather than AI infrastructure.
In the next chapter, we’ll run the complete system end-to-end, inspect the tool invocation flow, and see how the AI seamlessly coordinates multiple MCP servers to answer complex user requests.