MCP Integration with Spring AI: Step-by-Step Guide
A detailed guide to build MCP server and client applications with Spring AI, ChatClient, Streamable HTTP, MCP tools, resources, and real business examples.
MCP stands for Model Context Protocol.
In simple words:
MCP is a standard way for AI applications to discover and call external tools, resources, and prompts.
Without MCP, every AI app needs custom integration code for every service:
- One custom integration for order APIs.
- One custom integration for inventory APIs.
- One custom integration for file systems.
- One custom integration for database tools.
- One custom integration for ticketing systems.
MCP gives a common client/server protocol so AI applications can connect to external capabilities in a reusable way.
Spring AI supports both sides:
- Build an MCP client that connects to MCP servers.
- Build an MCP server that exposes Spring services as MCP tools/resources/prompts.
In this guide, we will build both.
What We Are Building
We will build a real retail-support example with two Spring Boot apps:
| App | Port | Purpose |
|---|---|---|
inventory-mcp-server |
8081 |
Exposes inventory/order tools over MCP |
support-ai-client |
8080 |
Chat assistant that connects to the MCP server and uses its tools |
The user will ask:
Can we ship SKU-1001 to Dallas?
The AI client will use MCP tools from the server:
get_inventorycheck_shipping_regionreserve_inventory
Then it will answer:
Yes, SKU-1001 is available and can be shipped to Dallas. I reserved 1 unit. Reservation ID: RSV-123456.
MCP Architecture
flowchart LR
User["User"] --> ClientAPI["Spring AI Client API"]
ClientAPI --> ChatClient["ChatClient"]
ChatClient --> Model["AI Model"]
ChatClient --> MCPClient["Spring AI MCP Client"]
MCPClient --> MCPServer["Spring AI MCP Server"]
MCPServer --> Tools["Inventory / Order Tools"]
MCPServer --> Resources["Business Resources"]
Tools --> Data["Inventory Data"]
Resources --> Data
The important point:
The AI model does not directly call your database. It asks the Spring AI client to call an MCP tool, and the MCP server executes controlled Java code.
MCP Concepts
| Concept | Meaning |
|---|---|
| MCP Host | The AI application that wants external capabilities |
| MCP Client | Component inside the host that connects to MCP servers |
| MCP Server | Application that exposes tools, resources, and prompts |
| Tool | Function the model can ask to execute |
| Resource | Data exposed through a URI-like interface |
| Prompt | Reusable prompt template exposed by a server |
| Transport | How client/server communicate: STDIO, SSE, Streamable HTTP, Stateless |
Spring AI MCP Support
Spring AI 2.0 provides MCP integration through Boot starters:
| Need | Starter |
|---|---|
| MCP client with STDIO, SSE, Streamable HTTP | spring-ai-starter-mcp-client |
| MCP client with WebFlux transport | spring-ai-starter-mcp-client-webflux |
| MCP server with STDIO | spring-ai-starter-mcp-server |
| MCP server with WebMVC HTTP transport | spring-ai-starter-mcp-server-webmvc |
| MCP server with WebFlux HTTP transport | spring-ai-starter-mcp-server-webflux |
For this tutorial, we will use:
spring-ai-starter-mcp-server-webmvcspring-ai-starter-mcp-client
We will use Streamable HTTP because Spring AI 2.0 recommends it over the older SSE-only setup for HTTP-based MCP servers.
Final Flow
sequenceDiagram
participant U as User
participant A as support-ai-client
participant LLM as AI Model
participant C as MCP Client
participant S as inventory-mcp-server
participant T as Inventory Tool
U->>A: Ask inventory/shipping question
A->>LLM: Prompt + MCP tool definitions
LLM-->>A: Tool call request
A->>C: Execute MCP tool
C->>S: JSON-RPC tool call over HTTP
S->>T: Run Java method
T-->>S: Tool result
S-->>C: MCP response
C-->>A: Tool result
A->>LLM: Tool result
LLM-->>A: Final answer
A-->>U: JSON response
Prerequisites
| Tool | Recommended Version |
|---|---|
| Java | 21 or later |
| Maven | 3.9+ |
| Spring Boot | 4.0.x |
| Spring AI | 2.0.0 |
| OpenAI API key | Required for the client app |
| curl or Postman | For testing |
Set your OpenAI key:
export OPENAI_API_KEY="your-openai-api-key-here"
Windows PowerShell:
$env:OPENAI_API_KEY="your-openai-api-key-here"
Part 1: Build the MCP Server
The MCP server exposes inventory tools to any MCP-compatible client.
Project name:
inventory-mcp-server
Server Project Structure
inventory-mcp-server/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/
│ └── codewithvenu/
│ └── inventorymcp/
│ ├── InventoryMcpServerApplication.java
│ ├── model/
│ │ ├── InventoryItem.java
│ │ ├── InventoryReservation.java
│ │ └── ShippingCheck.java
│ ├── repository/
│ │ └── InventoryRepository.java
│ └── tools/
│ └── InventoryMcpTools.java
└── resources/
└── application.yml
Step 1: Server pom.xml
File: inventory-mcp-server/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>4.0.0</version>
<relativePath/>
</parent>
<groupId>com.codewithvenu</groupId>
<artifactId>inventory-mcp-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>inventory-mcp-server</name>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</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-mcp-server-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 2: Server Configuration
File: inventory-mcp-server/src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: inventory-mcp-server
ai:
mcp:
server:
name: inventory-mcp-server
version: 1.0.0
type: SYNC
protocol: STREAMABLE
annotation-scanner:
enabled: true
Important settings:
protocol: STREAMABLEexposes the MCP server over Streamable HTTP.type: SYNCregisters synchronous MCP tools.annotation-scanner.enabled: truescans Spring beans for MCP annotations.
Step 3: Server Application Class
File: InventoryMcpServerApplication.java
package com.codewithvenu.inventorymcp;
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 4: Server Models
File: model/InventoryItem.java
package com.codewithvenu.inventorymcp.model;
import java.math.BigDecimal;
public record InventoryItem(
String sku,
String name,
int availableQuantity,
BigDecimal price,
String warehouse
) {
}
File: model/ShippingCheck.java
package com.codewithvenu.inventorymcp.model;
public record ShippingCheck(
String city,
boolean supported,
String estimatedDelivery,
String message
) {
}
File: model/InventoryReservation.java
package com.codewithvenu.inventorymcp.model;
import java.time.Instant;
public record InventoryReservation(
String reservationId,
String sku,
int quantity,
String city,
Instant createdAt
) {
}
Step 5: Server Repository
File: repository/InventoryRepository.java
package com.codewithvenu.inventorymcp.repository;
import com.codewithvenu.inventorymcp.model.InventoryItem;
import com.codewithvenu.inventorymcp.model.InventoryReservation;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Repository
public class InventoryRepository {
private final Map<String, InventoryItem> inventory = new ConcurrentHashMap<>();
private final Map<String, InventoryReservation> reservations = new ConcurrentHashMap<>();
public InventoryRepository() {
inventory.put("SKU-1001", new InventoryItem(
"SKU-1001",
"Spring AI Developer Guide",
12,
new BigDecimal("39.99"),
"Dallas-WH"
));
inventory.put("SKU-1002", new InventoryItem(
"SKU-1002",
"Java Interview Workbook",
0,
new BigDecimal("24.99"),
"Chicago-WH"
));
inventory.put("SKU-1003", new InventoryItem(
"SKU-1003",
"System Design Flashcards",
5,
new BigDecimal("14.99"),
"Austin-WH"
));
}
public Optional<InventoryItem> findBySku(String sku) {
return Optional.ofNullable(inventory.get(sku));
}
public List<InventoryItem> findAll() {
return new ArrayList<>(inventory.values());
}
public InventoryReservation reserve(String sku, int quantity, String city) {
InventoryItem item = findBySku(sku)
.orElseThrow(() -> new IllegalArgumentException("SKU not found: " + sku));
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be greater than zero");
}
if (item.availableQuantity() < quantity) {
throw new IllegalArgumentException("Not enough inventory for SKU: " + sku);
}
InventoryItem updated = new InventoryItem(
item.sku(),
item.name(),
item.availableQuantity() - quantity,
item.price(),
item.warehouse()
);
inventory.put(sku, updated);
String reservationId = "RSV-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
InventoryReservation reservation = new InventoryReservation(
reservationId,
sku,
quantity,
city,
Instant.now()
);
reservations.put(reservationId, reservation);
return reservation;
}
public List<InventoryReservation> findReservations() {
return new ArrayList<>(reservations.values());
}
}
Step 6: MCP Tools
File: tools/InventoryMcpTools.java
package com.codewithvenu.inventorymcp.tools;
import com.codewithvenu.inventorymcp.model.InventoryItem;
import com.codewithvenu.inventorymcp.model.InventoryReservation;
import com.codewithvenu.inventorymcp.model.ShippingCheck;
import com.codewithvenu.inventorymcp.repository.InventoryRepository;
import org.springframework.ai.mcp.annotation.McpResource;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Set;
@Component
public class InventoryMcpTools {
private final InventoryRepository repository;
private final Set<String> supportedCities = Set.of(
"Dallas",
"Austin",
"Houston",
"Chicago",
"New York"
);
public InventoryMcpTools(InventoryRepository repository) {
this.repository = repository;
}
@McpTool(name = "get_inventory", description = "Get inventory details for a SKU, including available quantity, price, and warehouse")
public InventoryItem getInventory(
@McpToolParam(description = "Product SKU such as SKU-1001", required = true) String sku
) {
return repository.findBySku(sku)
.orElseThrow(() -> new IllegalArgumentException("SKU not found: " + sku));
}
@McpTool(name = "check_shipping_region", description = "Check whether a city is supported for shipping and return estimated delivery")
public ShippingCheck checkShippingRegion(
@McpToolParam(description = "Destination city, for example Dallas", required = true) String city
) {
boolean supported = supportedCities.contains(city);
if (!supported) {
return new ShippingCheck(
city,
false,
"not available",
"Shipping is not currently supported for " + city
);
}
return new ShippingCheck(
city,
true,
"2 to 4 business days",
"Shipping is supported for " + city
);
}
@McpTool(name = "reserve_inventory", description = "Reserve available inventory for a SKU and destination city")
public InventoryReservation reserveInventory(
@McpToolParam(description = "Product SKU such as SKU-1001", required = true) String sku,
@McpToolParam(description = "Quantity to reserve", required = true) int quantity,
@McpToolParam(description = "Destination city", required = true) String city
) {
ShippingCheck shippingCheck = checkShippingRegion(city);
if (!shippingCheck.supported()) {
throw new IllegalArgumentException("Cannot reserve inventory because shipping is not supported for " + city);
}
return repository.reserve(sku, quantity, city);
}
@McpResource(uri = "inventory://all", name = "All Inventory")
public List<InventoryItem> allInventory() {
return repository.findAll();
}
}
The imports above use Spring AI MCP annotation names. If your IDE cannot resolve them, confirm that spring-ai-starter-mcp-server-webmvc is present and let the IDE auto-import from the Spring AI MCP annotation package used by your installed Spring AI version.
Step 7: Run the MCP Server
From the inventory-mcp-server folder:
mvn spring-boot:run
Expected startup:
Tomcat started on port 8081
Started InventoryMcpServerApplication
The MCP endpoint is exposed at:
http://localhost:8081/mcp
You normally do not call MCP endpoints manually like a REST API. MCP clients communicate with them using JSON-RPC over the selected transport.
Part 2: Build the Spring AI MCP Client
The client app is the user-facing AI assistant.
Project name:
support-ai-client
Client Project Structure
support-ai-client/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/
│ └── codewithvenu/
│ └── mcpclient/
│ ├── McpClientApplication.java
│ ├── controller/
│ │ └── SupportAssistantController.java
│ ├── dto/
│ │ ├── ChatRequest.java
│ │ └── ChatResponse.java
│ └── service/
│ └── SupportAssistantService.java
└── resources/
└── application.yml
Step 8: Client pom.xml
File: support-ai-client/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>4.0.0</version>
<relativePath/>
</parent>
<groupId>com.codewithvenu</groupId>
<artifactId>support-ai-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>support-ai-client</name>
<properties>
<java.version>21</java.version>
<spring-ai.version>2.0.0</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.boot</groupId>
<artifactId>spring-boot-starter-validation</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</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 9: Client Configuration
File: support-ai-client/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: support-ai-client
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4.1-mini
temperature: 0.2
mcp:
client:
enabled: true
name: support-ai-client
version: 1.0.0
request-timeout: 30s
type: SYNC
toolcallback:
enabled: true
streamable-http:
connections:
inventory-server:
url: http://localhost:8081
endpoint: /mcp
Important:
streamable-http.connections.inventory-server.urlpoints to the MCP server.endpoint: /mcpis the Streamable HTTP MCP endpoint.toolcallback.enabled: truemakes MCP tools available as Spring AIToolCallbacks.type: SYNCmeans the client will use synchronous MCP clients and tool callbacks.
Step 10: Client Application Class
File: McpClientApplication.java
package com.codewithvenu.mcpclient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class McpClientApplication {
public static void main(String[] args) {
SpringApplication.run(McpClientApplication.class, args);
}
}
Step 11: Client DTOs
File: dto/ChatRequest.java
package com.codewithvenu.mcpclient.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public record ChatRequest(
@NotBlank(message = "message is required")
@Size(max = 4000, message = "message must be less than 4000 characters")
String message
) {
}
File: dto/ChatResponse.java
package com.codewithvenu.mcpclient.dto;
import java.time.Instant;
public record ChatResponse(
String answer,
Instant createdAt
) {
public static ChatResponse of(String answer) {
return new ChatResponse(answer, Instant.now());
}
}
Step 12: Client Service
File: service/SupportAssistantService.java
package com.codewithvenu.mcpclient.service;
import com.codewithvenu.mcpclient.dto.ChatResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.stereotype.Service;
@Service
public class SupportAssistantService {
private final ChatClient chatClient;
private final SyncMcpToolCallbackProvider mcpTools;
public SupportAssistantService(ChatClient.Builder builder, SyncMcpToolCallbackProvider mcpTools) {
this.mcpTools = mcpTools;
this.chatClient = builder
.defaultSystem("""
You are a retail support assistant.
You can use MCP tools from the inventory server to:
- check inventory by SKU
- check whether shipping is supported for a city
- reserve inventory when the user clearly asks to reserve or buy
Rules:
- Ask for the SKU if missing.
- Ask for the destination city if shipping needs to be checked and city is missing.
- Do not reserve inventory unless the user asks to reserve, buy, or proceed.
- Explain tool results clearly.
""")
.build();
}
public ChatResponse chat(String message) {
String answer = chatClient
.prompt()
.user(message)
.tools(mcpTools)
.call()
.content();
return ChatResponse.of(answer);
}
}
SyncMcpToolCallbackProvider exposes MCP tools as Spring AI tools. ChatClient can use them with .tools(mcpTools).
If your IDE shows a package mismatch for SyncMcpToolCallbackProvider, use the class provided by the Spring AI MCP client starter in your installed version. The concept is the same: inject the MCP tool callback provider and pass it to ChatClient.tools(...).
Step 13: Client Controller
File: controller/SupportAssistantController.java
package com.codewithvenu.mcpclient.controller;
import com.codewithvenu.mcpclient.dto.ChatRequest;
import com.codewithvenu.mcpclient.dto.ChatResponse;
import com.codewithvenu.mcpclient.service.SupportAssistantService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
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.Map;
@RestController
@RequestMapping("/api/mcp-chat")
public class SupportAssistantController {
private final SupportAssistantService supportAssistantService;
public SupportAssistantController(SupportAssistantService supportAssistantService) {
this.supportAssistantService = supportAssistantService;
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "UP", "service", "support-ai-client");
}
@PostMapping
public ChatResponse chat(@Valid @RequestBody ChatRequest request) {
return supportAssistantService.chat(request.message());
}
}
Step 14: Run Both Apps
Terminal 1:
cd inventory-mcp-server
mvn spring-boot:run
Terminal 2:
cd support-ai-client
mvn spring-boot:run
Check client:
curl http://localhost:8080/api/mcp-chat/health
Expected output:
{
"service": "support-ai-client",
"status": "UP"
}
Step 15: Test MCP Tool Usage
Example 1: Inventory Check
Input:
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{
"message": "Do we have SKU-1001 in stock?"
}'
Expected behavior:
- Model sees the user asks about inventory.
- Model requests MCP tool
get_inventory. - Spring AI MCP client calls the MCP server.
- MCP server executes Java method.
- Model receives the tool result and answers.
Expected output:
{
"answer": "Yes. SKU-1001, Spring AI Developer Guide, is in stock with 12 units available. The price is 39.99 and it ships from Dallas-WH.",
"createdAt": "2026-06-23T10:15:30Z"
}
Example 2: Shipping Check
Input:
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{
"message": "Can you ship SKU-1001 to Dallas?"
}'
Expected output:
{
"answer": "Yes. SKU-1001 is available, and shipping to Dallas is supported. Estimated delivery is 2 to 4 business days.",
"createdAt": "2026-06-23T10:16:00Z"
}
Example 3: Reserve Inventory
Input:
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{
"message": "Please reserve 1 unit of SKU-1001 for Dallas."
}'
Expected output:
{
"answer": "I reserved 1 unit of SKU-1001 for Dallas. Reservation ID: RSV-8A91B2C3.",
"createdAt": "2026-06-23T10:17:00Z"
}
Example 4: Out of Stock
Input:
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{
"message": "Can I buy SKU-1002?"
}'
Expected output:
{
"answer": "SKU-1002 is currently out of stock, so I cannot reserve it right now.",
"createdAt": "2026-06-23T10:18:00Z"
}
MCP Server vs Normal REST API
| REST API | MCP Server |
|---|---|
| Built for application developers | Built for AI clients and agents |
| Client must know endpoint details | Client can discover available tools |
| Usually endpoint-specific | Tool/resource/prompt abstraction |
| Custom integration per AI app | Standard protocol across AI apps |
| Great for normal web apps | Great for AI tool ecosystems |
You can still use REST behind the MCP server. MCP is often a wrapper around existing APIs.
When To Use MCP
Use MCP when:
- Multiple AI apps need the same tools.
- You want dynamic tool discovery.
- You want a standard tool interface.
- You want to expose internal systems to AI clients safely.
- You are building an agent platform.
- You want compatibility with MCP-capable clients.
Use normal Spring AI tool calling when:
- Tools are only used inside one app.
- You do not need cross-client reuse.
- You want the simplest possible implementation.
Transport Choices
| Transport | Best For |
|---|---|
| STDIO | Local tools, desktop apps, development utilities |
| Streamable HTTP | Remote MCP servers, microservices, production services |
| Stateless Streamable HTTP | Cloud-native stateless deployments |
| SSE | Older HTTP streaming style; use Streamable HTTP for new Spring AI 2.0 apps |
For this guide, Streamable HTTP is the best fit because the server and client are separate Spring Boot applications.
Security Checklist
MCP does not remove the need for application security.
For production:
- Authenticate MCP clients.
- Authorize each tool action.
- Validate tool parameters in Java.
- Do not expose dangerous tools broadly.
- Use tenant/user context for data access.
- Log every tool call.
- Rate-limit expensive tools.
- Add confirmation for high-impact actions.
- Avoid returning secrets in tool results.
- Treat external MCP servers as untrusted until reviewed.
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Starting client before server | Client cannot connect | Start MCP server first |
| Wrong endpoint | Client cannot find MCP server | Use /mcp for Streamable HTTP |
| Using SSE for new HTTP server | Older pattern | Prefer STREAMABLE |
| No tool descriptions | Model calls tools poorly | Write clear @McpTool descriptions |
| Allowing action tools without validation | Unsafe behavior | Validate inside Java methods |
| Exposing too many tools | Confusing model behavior | Keep each MCP server focused |
| No audit logs | Hard to debug | Log tool name, arguments, result, user |
Complete Test Script
Run server first:
cd inventory-mcp-server
mvn spring-boot:run
Run client second:
cd support-ai-client
mvn spring-boot:run
Test:
curl http://localhost:8080/api/mcp-chat/health
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{"message":"Do we have SKU-1001 in stock?"}'
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{"message":"Can you ship SKU-1001 to Dallas?"}'
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{"message":"Please reserve 1 unit of SKU-1001 for Dallas."}'
curl -X POST http://localhost:8080/api/mcp-chat \
-H "Content-Type: application/json" \
-d '{"message":"Can I buy SKU-1002?"}'
Summary
You built a Spring AI MCP integration with two applications:
inventory-mcp-serverexposes tools through MCP.support-ai-clientconnects to the MCP server.ChatClientreceives MCP tools through the MCP tool callback provider.- The AI model can request tool calls.
- Spring AI executes those calls through the MCP client/server protocol.
- The final answer is returned to the user.
The key design idea:
MCP is best when you want AI tools to be reusable across many clients, not locked inside one application.
This pattern can be extended to:
- CRM tools.
- Database query tools.
- File search tools.
- DevOps tools.
- Ticketing systems.
- Banking tools.
- Insurance claim tools.
- Enterprise knowledge systems.