Enterprise Java

MCP for Java Developers: A Practical Tutorial With Spring AI and the MCP Java SDK

The Model Context Protocol finally gives Java a universal plug for AI. Here is how to wire up your first MCP server and client in a Spring Boot application — from scratch, with working code.

If you have been building AI-powered Java applications over the last year, you have almost certainly hit the same wall. You write a Spring Boot service that wraps an LLM. Then you need to give that LLM access to a database, an external API, or some internal business logic. So you write a custom integration — some adapter, some callback, some hand-rolled function-calling wrapper. It works, but it is fragile, bespoke, and the next AI tool you adopt will need its own version of the same plumbing.

The Model Context Protocol (MCP) was designed specifically to solve this problem. Introduced by Anthropic and standardised as an open protocol, MCP is essentially a universal adapter between AI models and the data sources, tools, and services they need to be useful. Think of it as USB-C for AI integrations: build one MCP server for your business logic, and any MCP-compatible client — Claude Desktop, VS Code Copilot, your own Spring application — can connect to it immediately.

In December 2024, the Spring team announced the MCP Java SDK, which they subsequently donated to Anthropic as the official Java implementation of the protocol. Spring AI 1.0 then built first-class Boot Starters on top of it, and since Spring AI 1.1.0, annotation-based development has made building MCP servers almost as simple as writing a regular Spring service. Yet almost no Java-specific tutorials exist. This one fills that gap.

You will need Java 17 or later, Maven or Gradle, and a basic familiarity with Spring Boot. To actually call an LLM in the client section, you will need an OpenAI API key — but you can follow all server-side steps without one. All code examples in this article are based on the current Spring AI MCP reference documentation and have been cross-checked against the live SDK.

1. What MCP Actually Is (and What It Is Not)

Before diving into code, it is worth spending two minutes on the mental model, because MCP is often misunderstood. It is not a new LLM. It is not a framework. It is a protocol — a specification for how processes communicate, similar in spirit to how HTTP defines how browsers and servers communicate.

Concretely, MCP defines three primitives that a server can expose to any connected client. Tools are callable functions — things the AI can invoke to take action or retrieve data. Resources are readable data sources, similar to files or database rows, that the AI can include in its context. Prompts are reusable, parameterised prompt templates that standardise how common queries are phrased. An MCP server exposes some combination of these three, and an MCP client discovers and invokes them using a standardised JSON-RPC 2.0 message format.

The key design point is that MCP clients maintain one-to-one connections with servers, but a single host application can hold many MCP clients — and therefore connect to many servers simultaneously. Your Spring AI application, for instance, might simultaneously connect to an MCP server wrapping your PostgreSQL database, another wrapping a third-party weather API, and a third exposing your own internal order management logic. The LLM in the middle sees all their tools as a single flat list and decides which ones to call based on the user’s question.

The two transport options you will encounter

MCP supports two transport mechanisms, and which one you choose has real deployment consequences, so it is worth being clear here before writing any code.

TransportHow it worksBest forSpring starter
StdioThe client launches the MCP server as a child process and communicates via stdin/stdout over JSON-RPC newlinesLocal tools, developer machines, Claude Desktop integrationspring-ai-starter-mcp-server
Streamable HTTPSingle HTTP endpoint (/mcp) supports both POST for requests and optional SSE streaming for responses. Replaced the older two-endpoint SSE transport in the March 2025 spec updateRemote servers, Kubernetes, multi-user production deployments, cloud environmentsspring-ai-starter-mcp-server-webmvc

If you see older tutorials using an /sse endpoint alongside a separate /sse/messages POST endpoint, that is the deprecated HTTP+SSE transport from the November 2024 spec. It has been superseded by Streamable HTTP and several major MCP clients have published sunset dates for SSE support in mid-2026. Use Streamable HTTP for all new remote server implementations.

2. Building Your First MCP Server With Spring Boot

Let’s build something concrete: an MCP server that exposes a small product catalogue as tools and a resource. This is a realistic pattern — wrapping your own domain data so AI clients can query it. The server will run over Streamable HTTP, which means it can be deployed as a normal Spring Boot application and reached from any MCP client over HTTP.

Step 1: Create the project

Head to start.spring.io and create a Maven project with Java 17+, Spring Boot 3.3+, and add the following dependency. Alternatively, add it directly to your pom.xml:

<dependencies>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

Step 2: Configure the server transport

In application.properties, enable the Streamable HTTP transport. This is the most important configuration step — without it, Spring AI defaults to the stdio transport, which will not expose an HTTP endpoint.

# Name shown during MCP capability negotiation
spring.ai.mcp.server.name=product-catalogue-server
spring.ai.mcp.server.version=1.0.0

# Enable the Streamable HTTP transport (single /mcp endpoint)
spring.ai.mcp.server.transport=STREAMABLE_HTTP

# Server port — the MCP endpoint will be at http://localhost:8081/mcp
server.port=8081

Step 3: Define your domain model

Java records are ideal for MCP tool parameters and return types. Spring AI automatically serialises them into JSON schemas that the LLM uses to understand what data to pass and what to expect back. Keep records simple and descriptive — the field names become part of the schema.

package com.example.mcp.model;

public record Product(
    String id,
    String name,
    String category,
    double priceUsd,
    boolean inStock
) {}
package com.example.mcp.service;

import com.example.mcp.model.Product;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@Service
public class ProductCatalogueService {

    // In a real application this would be a JPA repository or similar
    private static final List<Product> CATALOGUE = List.of(
        new Product("P001", "Laptop Pro 15", "Electronics", 1299.99, true),
        new Product("P002", "Wireless Keyboard", "Electronics", 89.99, true),
        new Product("P003", "Office Chair", "Furniture", 449.00, false),
        new Product("P004", "Standing Desk", "Furniture", 799.00, true),
        new Product("P005", "USB-C Hub", "Electronics", 49.99, true)
    );

    @Tool(description = """
            Search the product catalogue. Optionally filter by category
            (e.g. 'Electronics', 'Furniture') and/or whether the item
            is currently in stock. Returns a list of matching products
            with their prices in USD.
            """)
    public List<Product> searchProducts(
            @ToolParam(description = "Category to filter by. Leave blank for all categories.") String category,
            @ToolParam(description = "Set true to return only in-stock items.") boolean inStockOnly) {

        return CATALOGUE.stream()
            .filter(p -> category == null || category.isBlank()
                       || p.category().equalsIgnoreCase(category))
            .filter(p -> !inStockOnly || p.inStock())
            .toList();
    }

    @Tool(description = """
            Retrieve a single product by its unique product ID.
            Returns the full product details including current price and
            availability, or null if no product with that ID exists.
            """)
    public Product getProductById(
            @ToolParam(description = "The unique product ID, e.g. P001") String productId) {

        return CATALOGUE.stream()
            .filter(p -> p.id().equalsIgnoreCase(productId))
            .findFirst()
            .orElse(null);
    }

    @Tool(description = """
            Return a summary of the catalogue: total number of products,
            breakdown by category, and count of in-stock vs. out-of-stock items.
            Useful for getting a high-level overview before drilling into details.
            """)
    public Map<String, Object> getCatalogueSummary() {
        long inStock = CATALOGUE.stream().filter(Product::inStock).count();
        Map<String, Long> byCategory = CATALOGUE.stream()
            .collect(java.util.stream.Collectors.groupingBy(
                Product::category, java.util.stream.Collectors.counting()));

        return Map.of(
            "totalProducts", CATALOGUE.size(),
            "inStock", inStock,
            "outOfStock", CATALOGUE.size() - inStock,
            "byCategory", byCategory
        );
    }
}

In Spring AI 1.0, the annotations are @Tool and @ToolParam from org.springframework.ai.tool.annotation. Spring AI 1.1+ introduced @McpTool and @McpToolParam as MCP-specific aliases. Both work — the underlying mechanism is identical. This tutorial uses the 1.0-stable annotations for broadest compatibility. Check the reference documentation if you are on a newer milestone.

Step 5: Register tools with Spring AI’s auto-configuration

This is where most tutorials get verbose, but Spring AI’s auto-configuration genuinely handles it for you. If your bean is in the Spring context and its methods are annotated with @Tool, they are automatically discovered and registered as MCP tools. You need only annotate your main class and run the application.

package com.example.mcp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ProductCatalogueServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProductCatalogueServerApplication.class, args);
    }
}

Start the application with ./mvnw spring-boot:run. Your MCP server is now live. Any MCP-compatible client can connect to http://localhost:8081/mcp and immediately discover the three tools you just defined — no additional wiring required.

3. Adding an MCP Resource

Tools are callable functions — things an AI invokes to get data. Resources are slightly different: they are passive data that an AI can read and include in its context, more like files or document fragments. Think of resources as “things the AI can look at” versus tools as “things the AI can do.” Together, they give you fine-grained control over what the model can access and how.

In Spring AI 1.1+, resources are declared with @McpResource. In Spring AI 1.0, you register them programmatically via a ResourceRegistrationBean or by using the lower-level McpSyncServer API. The following example uses the annotation approach, which is available from Spring AI 1.1.0-M1 onwards:

package com.example.mcp.service;

import org.springframework.ai.mcp.server.annotation.McpResource;
import org.springframework.stereotype.Component;

@Component
public class CatalogueResourceProvider {

    // The uri template uses {format} as a path variable
    // so clients can request either "json" or "text" representations
    @McpResource(
        uri = "catalogue://overview/{format}",
        name = "Catalogue Overview",
        description = "A high-level overview of the product catalogue in the requested format."
    )
    public String getCatalogueOverview(String format) {
        if ("json".equalsIgnoreCase(format)) {
            return """
                {
                  "name": "Product Catalogue",
                  "totalProducts": 5,
                  "categories": ["Electronics", "Furniture"],
                  "lastUpdated": "2026-04-24"
                }
                """;
        }
        return "Product Catalogue — 5 products across 2 categories. Last updated: April 2026.";
    }
}

4. Building the MCP Client — Connecting an LLM to Your Server

Now that the server is running, let’s build the other side: a Spring Boot application that acts as an MCP client, connects to our product catalogue server, and lets a user ask questions about products in natural language. The LLM will decide which tools to call based on the question and assemble the answer from the results.

Project setup for the client

Create a separate Spring Boot project (or a separate module in the same multi-module build). You need the MCP client starter and a model starter — this example uses OpenAI, but you can swap in any Spring AI-supported model.

<dependencies>
    <!-- MCP client auto-configuration -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-mcp-client</artifactId>
    </dependency>

    <!-- LLM — swap for spring-ai-starter-model-anthropic, -ollama, etc. -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>

    <!-- REST endpoint to expose our chatbot -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Configure the client to connect to your server

This is the cleanest part of the Spring AI MCP integration. You declare the MCP server connection entirely in YAML — no Java code needed for the client configuration. Spring AI creates and manages the connection pool automatically.

spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: gpt-4o-mini

    mcp:
      client:
        # Use Streamable HTTP to connect to our running server
        streamable-http:
          connections:
            product-server:
              url: http://localhost:8081

Wire up the ChatClient with MCP tools

The ToolCallbackProvider bean is the key Spring AI abstraction here. It is automatically populated with all the tools discovered from the connected MCP servers. You pass it to the ChatClient at call time, and Spring AI handles the entire function-calling lifecycle: it sends the tool schemas to the LLM with the initial prompt, the LLM responds with tool calls, Spring AI executes them against the MCP server, and the results are sent back to the LLM for a final response. All of this happens transparently.

package com.example.mcpclient.controller;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/chat")
public class ProductChatController {

    private final ChatClient chatClient;
    private final ToolCallbackProvider mcpTools;

    public ProductChatController(ChatClient.Builder builder,
                                 ToolCallbackProvider mcpTools) {
        this.chatClient = builder
            .defaultSystem("""
                You are a helpful product assistant for an online store.
                Use the available tools to answer questions about products,
                availability, and pricing. Always be concise and accurate.
                """)
            .build();
        this.mcpTools = mcpTools;
    }

    @PostMapping
    public ChatResponse chat(@RequestBody ChatRequest request) {
        String answer = chatClient
            .prompt(request.question())
            // Pass all MCP tools from all connected servers to the LLM
            .toolCallbacks(mcpTools)
            .call()
            .content();
        return new ChatResponse(answer);
    }

    public record ChatRequest(String question) {}
    public record ChatResponse(String answer) {}
}

Start the client application on port 8080 (or any other port), make sure the server is still running on 8081, and try a request:

curl -s -X POST http://localhost:8080/chat \
  -H "Content-Type: application/json" \
  -d '{"question": "Which electronics are currently in stock, and what are their prices?"}'

The response will be a natural-language summary assembled from the results of calling searchProducts with category="Electronics" and inStockOnly=true — entirely without you writing any explicit tool-dispatch logic.

5. Connecting a Stdio Server: Integrating Pre-Built MCP Servers

One of the immediate benefits of MCP is that a large ecosystem of pre-built servers already exists. Brave Search, filesystem access, GitHub, PostgreSQL, and dozens of others are available as off-the-shelf MCP servers — most are TypeScript-based and distributed as npm packages. Spring AI can connect to these through the stdio transport, launching them as child processes. This requires Node.js to be installed on the machine running your Spring AI client, but it opens up a huge catalogue of capabilities with zero additional server code.

To add a Brave Search server alongside your product catalogue server, simply extend the application.yml of your client:

spring:
  ai:
    mcp:
      client:
        # Streamable HTTP for our Java server
        streamable-http:
          connections:
            product-server:
              url: http://localhost:8081

        # Stdio for pre-built Node.js MCP servers
        stdio:
          connections:
            brave-search:
              command: npx
              args:
                - "-y"
                - "@modelcontextprotocol/server-brave-search"
              env:
                BRAVE_API_KEY: ${BRAVE_API_KEY}

After adding this, your ToolCallbackProvider will automatically include the Brave Search tools alongside your product tools. No code changes required. The LLM can now answer “Which of our in-stock products has received the best reviews online?” by calling searchProducts for your internal data and then using the Brave Search tool to look up reviews — all in a single conversation turn.

MCP Ecosystem: Transport usage by server type (2025)

Based on the MCP server registry and community surveys. Stdio dominates local/developer tooling; Streamable HTTP is the standard for production deployments.

6. stdio vs Streamable HTTP: When to Use Each

The choice of transport is not just a configuration detail — it shapes how you build, deploy, and secure your MCP integrations. Here is a practical decision guide.

ConcernStdioStreamable HTTP
DeploymentClient must be on same machine; server is a subprocessServer is an independent service; can be on any host
ScalingOne connection per client processMultiple clients, load balancers, Kubernetes-native
SecurityOS process isolation; no network exposureStandard HTTPS, OAuth 2, API keys — well-understood
Logs / ConsoleMust suppress all stdout logs — they corrupt the protocolNormal logging works fine
Best forClaude Desktop, VS Code Copilot, local dev tools, Node.js packagesProduction APIs, team-shared servers, cloud deployments
Future-proofStable, no deprecation plannedThe current standard — SSE is deprecated

Critical for Stdio Servers

If you build a Spring Boot MCP server with spring.ai.mcp.server.stdio=true, you must silence the Spring Boot banner and route all logs away from stdout. The protocol uses stdout exclusively for JSON-RPC messages, and any other output — including startup banners — corrupts the stream. Add these properties: spring.main.banner-mode=off and logging.pattern.console= (empty value disables console logging). Direct logs to a file instead.

7. The @McpResource Annotation and Prompt Templates

Beyond tools, MCP provides two other primitives that are worth understanding, even if you reach for them less often in early implementations.

Prompts: standardise how you query your data

An MCP prompt is a reusable, parameterised template that clients can ask the server to fill in. Think of it as a stored procedure for natural-language queries — you define the structure once on the server, and any client can request it with their specific parameters. This is valuable when you want to ensure consistent phrasing for common queries, or when you want to embed domain expertise into the prompt itself rather than leaving it up to each client to formulate the right question.

package com.example.mcp.service;

import org.springframework.ai.mcp.server.annotation.McpPrompt;
import org.springframework.ai.mcp.server.annotation.McpPromptParam;
import org.springframework.stereotype.Component;

@Component
public class ProductPromptProvider {

    @McpPrompt(
        name = "product-comparison",
        description = "Generate a structured comparison of two products by their IDs"
    )
    public String compareProducts(
            @McpPromptParam(description = "ID of the first product") String productId1,
            @McpPromptParam(description = "ID of the second product") String productId2) {

        return String.format("""
            Compare the products with IDs %s and %s.
            For each product, retrieve its name, category, price in USD,
            and current availability. Then provide a concise summary of
            which product offers better value for money, and for which
            use case each is more appropriate.
            """, productId1, productId2);
    }
}

8. Putting It All Together: Architecture Chart

Spring AI MCP: component interaction sequence (simplified)

How a user question travels through the stack and back. All tool dispatch and result assembly is handled automatically by Spring AI.

9. What We Learned

This tutorial covered the full MCP development cycle in Java — from protocol concepts through to a running server-client pair. Here is a concise recap of the most important points.

  • MCP is an open protocol — not a framework — that standardises how AI models connect to external tools, data sources, and prompt templates through a JSON-RPC 2.0 message layer.
  • The MCP Java SDK, donated to Anthropic by the Spring team in December 2024, is the official Java implementation. Spring AI 1.0 wraps it with Boot Starters and annotation-based auto-configuration.
  • Building an MCP server requires a single dependency (spring-ai-starter-mcp-server-webmvc), one property to enable Streamable HTTP, and Spring-managed beans with @Tool-annotated methods. Spring AI handles discovery, schema generation, and registration automatically.
  • The MCP client side is even simpler: declare server connections in YAML, inject the auto-provided ToolCallbackProvider, and pass it to ChatClient.toolCallbacks(). Spring AI manages the full tool-calling loop with the LLM.
  • Stdio transport is ideal for local tools and integrating pre-built Node.js MCP servers. Streamable HTTP is the correct choice for production, remote, and multi-client deployments — and replaces the now-deprecated HTTP+SSE transport.
  • Tool description quality is the single most impactful factor in real-world MCP performance. The LLM uses descriptions to decide what to call and when — invest time in writing them clearly.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button