From REST API to MCP Server: A Spring Boot Guide
Varsha Das · 5 November 2026
It is not an uncommon task to expose an existing Java service to a new consumer. It might be a mobile app, a partner integration or another team’s microservice. These days that new consumer is increasingly an AI agent, and most Java teams already have the business logic it needs, sitting behind years of Spring Boot microservices and REST APIs. So the elephant in the room is — how do we make hundreds of existing REST APIs usable by agents without rewriting them?
You don’t rewrite. You add a protocol layer. MCP lets AI models reach into that existing logic and use it to take real actions.
A common misconception is that you need deep AI experience before you can do this. You need the fundamentals, but if you know Spring Boot, you’re already most of the way there.
When an agent needs to use our service, there are a number of things we typically have to work out:
- How does the agent find out what the service can do? A human developer reads the API docs. An agent has to discover the available operations at runtime.
- What does the agent need to pass? Every operation needs a clear schema for its inputs. Otherwise the model guesses the parameter names and formats.
- How does the agent understand the result? Responses and errors need to be structured so the model can act on them, rather than a stack trace it cannot interpret.
- Which transport do we use? Is the agent running locally on the developer’s machine, or connecting to a remote service over HTTP?
- How do we keep it safe? An agent will call whatever it is allowed to call, so access needs to be controlled like any other API.
Here’s how to turn an existing Spring Boot service into an MCP server that answers these questions, test it, and connect a client to it.
What MCP is
Before MCP, every AI application connected to tools its own way: a custom function schema for one model vendor, a plugin format for another.
Java developers lived through this with JDBC, JMS and JAX-RS. Each time, the industry settled on a standard that made the plumbing invisible.
The Model Context Protocol (MCP) is that standard for AI integration. It’s an open, language-independent protocol, now governed by the Linux Foundation’s Agentic AI Foundation, for how AI applications discover and call external tools.
MCP is to AI agents what JDBC is to databases.
Every MCP setup has three parts:
- Host – the AI application the user talks to. It owns the conversation with the LLM.
- Client – one connection to one server inside the host, like a WebClient per service.
- Server – exposes tools over the protocol. This is your Spring Boot app.
What MCP actually solves
An AI model is only as useful as what it can reach. If it can’t call your services, it gives generic answers. Once it can, its answers rest on your real data and business rules.
Without MCP, you write a one-off integration for every AI client. With MCP, you build one server, and any MCP-compatible client can connect to it and discover your tools:
- Desktop assistants – Claude Desktop, ChatGPT desktop, goose
- IDEs and AI coding tools – VS Code with GitHub Copilot, IntelliJ IDEA with JetBrains AI Assistant, Cursor, Windsurf, Kiro
- Command-line agents – Claude Code, Gemini CLI, Kiro CLI
- Your own apps – a Spring Boot application using Spring AI’s MCP client, or a Java app built with LangChain4j or Quarkus
When to use MCP
- Use it when your logic already lives in well-tested Java services. The validation, business rules and data access are already written and running in production. MCP lets an agent use them as they are, instead of you rebuilding them inside an AI app.
- Use it when more than one client needs the same capabilities. If an IDE assistant, a chatbot and an internal agent all need to look up orders, one MCP server serves all three. Without it, you’d write and maintain three separate integrations.
- Use it when tools change faster than the AI app. Because clients discover tools at runtime, you can add or update a tool by redeploying only the server. The agents that use it don’t need a release.
- Skip it when one app calls one model with a couple of functions. Plain tool calling with
@Toolinside the app is simpler. A protocol layer only pays off once something else needs to reuse those tools. - Skip it when the logic doesn’t exist yet. If you’re building a feature from scratch for a single AI app, start with in-app tool calling. You can move it behind an MCP server later, and the method stays the same.
Don’t expect it to fix the API behind it. MCP simplifies the contract between the agent and your service, not the service itself. A slow API still means a slow agent, and an unclear API still means an agent that guesses. If an endpoint is confusing for a human developer, it will be confusing for a model too.
A case study: an order service
Let’s take a typical Spring Boot service: an order API that returns the status of a customer order. We’re building an MCP server that lets an AI look up any order and report its status and expected delivery date, using the service and controller we already have, without changing either.
Prerequisites
- Java 21
- Spring Boot 4.1
- Maven
- Node.js, for the MCP Inspector
You don’t need an AI model or API key to build and test the server.
Project setup
Start from start.spring.io with Spring Web, then import the Spring AI BOM so all Spring AI modules share one version:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Then add the MCP server starter for Spring MVC:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
</dependency>
On a reactive stack, use spring-ai-starter-mcp-server-webflux instead. Your tool code is identical either way.
The existing service and controller
This is the code you already have. The service holds the business logic:
public record OrderStatus(String orderId, String status, LocalDate expectedDelivery) {}
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public OrderStatus findStatus(String orderId) {
// existing, tested business logic
return orderRepository.findStatus(orderId);
}
}
The controller exposes it over REST:
@RestController
@RequestMapping("/orders")
class OrderController {
private final OrderService orderService;
OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{orderId}/status")
OrderStatus status(@PathVariable String orderId) {
return orderService.findStatus(orderId);
}
}
Neither of these changes.
Why not just put @McpTool on the controller method? You could. But a tool isn’t an endpoint.
Anthropic says the most common mistake they see is tools that simply wrap existing API endpoints. So keep the tool layer thin, and shape it around what the agent is trying to do, not around your URLs.
Add the MCP tool
Add a component that exposes the same service as a tool:
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpToolParam;
@Component
public class OrderTools {
private final OrderService orderService;
public OrderTools(OrderService orderService) {
this.orderService = orderService;
}
@McpTool(name = "get-order-status",
description = "Get the current status and expected delivery date of a customer order.")
public OrderStatus getOrderStatus(
@McpToolParam(description = "The order ID, e.g. ORD-10293", required = true) String orderId) {
return orderService.findStatus(orderId);
}
}
At startup, Spring AI scans for @McpTool methods, generates the JSON schema from the method signature, and registers the tool.
The description matters more than it looks. An agent never sees your code. It decides from the name, description and schema alone, so say what the tool does, what it returns, and give an example for each parameter.
@McpTool vs @Tool
Spring AI has two tool annotations, and they are not interchangeable:
@Tool(with@ToolParam) – a tool used by your own ChatClient, inside one application.@McpTool(with@McpToolParam) – a tool exposed by an MCP server, for any MCP client to discover and call.
Keep each pair together. Mixing @McpTool with @ToolParam is a common mistake.
Choose the transport
This is where most developers hit their first problem. Spring AI supports several transports:
- STDIO – the server runs as a local subprocess of the client. Use
spring-ai-starter-mcp-serverwithspring.ai.mcp.server.stdio=true. - SSE – the original HTTP transport. Deprecated.
- Streamable HTTP – the recommended transport for remote servers. One endpoint,
/mcp. - Stateless – Streamable HTTP without sessions (
protocol=STATELESS), for multiple replicas without sticky sessions.
Set it explicitly:
spring.ai.mcp.server.protocol=STREAMABLE
spring.ai.mcp.server.name=order-server
spring.ai.mcp.server.version=1.0.0
Don’t skip protocol=STREAMABLE. Without it, the WebMVC starter serves the deprecated SSE transport and /mcp returns 404.
Run and test the server
Start the application:
./mvnw spring-boot:run
List the tools with curl
Every MCP session starts with initialize:
curl -i -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
The response includes an Mcp-Session-Id header. Send it on every request that follows, starting with the initialized notification that completes the handshake:
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: <session-id>" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
Then list the tools:
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: <session-id>" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
You’ll see get-order-status with its description and input schema. This is exactly what an agent sees.
Use the MCP Inspector
For a browser-based view, run the official MCP Inspector:
npx @modelcontextprotocol/inspector
Choose Streamable HTTP, connect to http://localhost:8080/mcp, and open the Tools tab. You can call get-order-status with a real order ID and see the result.
Connect a client
Once the server works, there are three common ways to connect to it:
- Your own Spring Boot client – add
spring-ai-starter-mcp-clientto another app and point it at the server. Its ChatClient can then use the remote tools. - Claude Desktop – add the server to Claude Desktop’s configuration (for a remote HTTP server, through a bridge such as mcp-remote) and ask it about an order.
- The MCP Inspector – as above. The quickest way to check what your server exposes, before any agent touches it.
The server doesn’t change in any of these. That’s the point of a protocol.
Your API was always the hard part, and you’ve already built it.
So, how do we make hundreds of existing REST APIs usable by agents without rewriting them?
One service at a time, one annotation at a time.
Your API was always the hard part, and you’ve already built it.
Pick one service this week. Add the starter, annotate one method and open the MCP Inspector. You’ll see your own code the way an agent sees it.
What you’ve built here works. Making it production-ready is the next step, and that’s what my session Your REST API Already Speaks AI: Building MCP Servers in Java covers at ChurConf Sydney, November 18–19.