Full Stack • Java • System Design • Cloud • AI Engineering

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:

  1. get_inventory
  2. check_shipping_region
  3. reserve_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-webmvc
  • spring-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: STREAMABLE exposes the MCP server over Streamable HTTP.
  • type: SYNC registers synchronous MCP tools.
  • annotation-scanner.enabled: true scans 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.url points to the MCP server.
  • endpoint: /mcp is the Streamable HTTP MCP endpoint.
  • toolcallback.enabled: true makes MCP tools available as Spring AI ToolCallbacks.
  • type: SYNC means 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:

  1. Model sees the user asks about inventory.
  2. Model requests MCP tool get_inventory.
  3. Spring AI MCP client calls the MCP server.
  4. MCP server executes Java method.
  5. 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:

  1. Authenticate MCP clients.
  2. Authorize each tool action.
  3. Validate tool parameters in Java.
  4. Do not expose dangerous tools broadly.
  5. Use tenant/user context for data access.
  6. Log every tool call.
  7. Rate-limit expensive tools.
  8. Add confirmation for high-impact actions.
  9. Avoid returning secrets in tool results.
  10. 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:

  1. inventory-mcp-server exposes tools through MCP.
  2. support-ai-client connects to the MCP server.
  3. ChatClient receives MCP tools through the MCP tool callback provider.
  4. The AI model can request tool calls.
  5. Spring AI executes those calls through the MCP client/server protocol.
  6. 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.

References