How to Build a Java MCP Server for AI Tools with Spring AI
This guide explains the Model Context Protocol (MCP), its JSON‑RPC communication modes, compares Java implementations, and provides a step‑by‑step tutorial—including project setup, configuration, tool creation, registration, and integration with Claude Desktop and IDEA—while covering description best practices, security, validation, and production considerations.
What problem MCP solves
Before MCP, each AI (Claude, GPT, Cursor) required its own plugin adapter for every business system, leading to an M×N explosion of adapters. MCP defines a standard protocol that reduces the integration effort to M+N, similar to the Language Server Protocol.
The protocol defines three capabilities: Tools (functions AI can call), Resources (data sources), and Prompts (template prompts). The article focuses on Tools, which cover 90% of business scenarios.
Underlying communication mechanism
MCP uses JSON‑RPC 2.0 and supports two transport modes:
stdio mode : the AI client launches the MCP server as a subprocess and communicates via standard input/output; ideal for local development with zero network configuration.
SSE mode : HTTP server‑sent events for remote deployment, allowing multiple clients to share a single MCP server.
A complete tool‑call flow consists of six steps:
AI client sends an initialize request when starting.
Server returns the list of supported Tools (name, description, parameter schema).
User initiates a conversation; the AI decides which Tool to call.
AI sends a tools/call request with the chosen Tool name and arguments.
Server executes the business logic and returns the result.
AI incorporates the result into its reply.
The description field of a Tool is critical; a well‑written description determines whether the AI can correctly understand and invoke the interface.
Java implementation options
Two mainstream choices exist in the Java ecosystem:
Spring AI MCP – maintained by the Spring team, version 1.0 stable, low integration difficulty (native Spring Boot), excellent documentation, best for existing Spring projects.
MCP4J – community‑driven, still active development, moderate integration difficulty, average documentation, suitable for standalone MCP services.
The tutorial adopts Spring AI MCP because most developers already use Spring Boot, resulting in zero additional learning cost.
Step‑by‑step: building an MCP Server
Project initialization
<dependencies>
<!-- Spring AI MCP Server -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>
<!-- Web (required for SSE mode) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>application.yml configuration
spring:
ai:
mcp:
server:
name: my-business-mcp-server
version: 1.0.0
# stdio mode
transport: stdio
# For SSE mode, change to:
# transport: sseWrite the first Tool
Assume an order‑query service that Claude can call directly.
@Service
public class OrderService {
private static final Map<String, Order> orders = Map.of(
"ORD001", new Order("ORD001", "张三", 299.0, "PENDING"),
"ORD002", new Order("ORD002", "李四", 599.0, "SHIPPED"),
"ORD003", new Order("ORD003", "王五", 199.0, "DELIVERED")
);
/**
* This annotation registers the method as an MCP Tool.
* The description is crucial – AI decides when to call based on it.
*/
@Tool(description = "根据订单ID查询订单信息,包括客户名称、金额和配送状态")
public Order getOrderById(@ToolParam(description = "订单ID,格式为 ORD 开头加三位数字,例如 ORD001") String orderId) {
Order order = orders.get(orderId);
if (order == null) {
throw new RuntimeException("订单不存在: " + orderId);
}
return order;
}
@Tool(description = "查询指定客户的所有订单列表")
public List<Order> getOrdersByCustomer(@ToolParam(description = "客户姓名") String customerName) {
return orders.values().stream()
.filter(o -> o.getCustomerName().equals(customerName))
.collect(Collectors.toList());
}
@Tool(description = "统计各状态订单数量,返回 PENDING/SHIPPED/DELIVERED 各自的数量")
public Map<String, Long> getOrderStatistics() {
return orders.values().stream()
.collect(Collectors.groupingBy(Order::getStatus, Collectors.counting()));
}
}
public record Order(String orderId, String customerName, Double amount, String status) {}Register Tool to MCP Server
@Configuration
public class McpConfig {
@Bean
public ToolCallbackProvider orderTools(OrderService orderService) {
// Spring AI automatically scans @Tool‑annotated methods
return MethodToolCallbackProvider.builder()
.toolObjects(orderService)
.build();
}
}Launch class
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}Run mvn spring-boot:run and the MCP server starts.
Connect Claude Desktop for verification
Configuration file locations:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows: %APPDATA%\Claude\claude_desktop_config.json Add an entry to mcpServers:
{
"mcpServers": {
"my-order-service": {
"command": "java",
"args": ["-jar", "/your/project/path/target/mcp-server-1.0.0.jar"]
}
}
}Restart Claude Desktop; the tool icon appears. Example conversation:
You: 帮我查一下 ORD002 这个订单的情况 Claude: 我来帮你查询一下。 [调用 getOrderById,参数:orderId="ORD002"] 查询结果: 订单号:ORD002 客户:李四 金额:¥599.0 状态:已发货(SHIPPED)
Connect IDEA Claude Code plugin
Add the same mcpServers entry to ~/.claude.json, then restart the Claude Code plugin in IDEA. The plugin automatically loads the MCP server.
Writing effective Tool descriptions
The AI decides whether to call a Tool solely based on the description field. A vague description leads to missed or wrong calls, while a detailed description guides the AI precisely.
Bad example :
@Tool(description = "查询订单")
public Order getOrderById(String orderId) { ... }Problems: ambiguous purpose, no parameter details, no return information, no usage scenario.
Good example :
@Tool(description = """
根据订单ID查询单个订单的详细信息。
返回内容包括:客户姓名、订单金额、当前配送状态(PENDING/SHIPPED/DELIVERED)。
当用户询问具体订单的状态、金额或客户信息时使用此工具。
""")
public Order getOrderById(@ToolParam(description = "订单唯一标识符,格式为 ORD 开头加三位数字,例如 ORD001、ORD002") String orderId) { ... }Key elements for a description (illustrated in a table in the original article) are:
Function statement : what the tool does.
Return content : which fields are returned.
Trigger scenario : when to use it.
Parameter format : constraints on inputs.
Boundary conditions : cases where the tool should not be used.
Production‑grade considerations
Permission control
@Tool(description = "查询订单信息")
public Order getOrderById(String orderId) {
// Retrieve caller identity from MCP context (supported in Spring AI 1.0)
// Apply business‑level permission checks
validatePermission(orderId);
return orderRepository.findById(orderId);
}Parameter validation
@Tool(description = "查询订单")
public Order getOrderById(@ToolParam(description = "订单ID") String orderId) {
// Format validation
if (!orderId.matches("ORD\\d{3}")) {
throw new IllegalArgumentException("订单ID格式错误: " + orderId);
}
return orderService.getById(orderId);
}Return‑value design
Avoid returning JPA entities directly; instead return DTOs that contain only the fields the AI needs.
// Bad – returns JPA entity ❌
public OrderEntity getOrder(String id) { ... }
// Good – returns DTO ✅
public OrderDTO getOrder(String id) {
OrderEntity entity = repository.findById(id);
return OrderDTO.from(entity); // includes only AI‑relevant fields
}Error handling
Provide human‑readable error messages so the AI can relay them to the user.
@Tool(description = "查询订单")
public Order getOrderById(String orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException(
String.format("未找到订单 %s,请确认订单号是否正确", orderId)
));
}Advanced: integrating a real database
Replace the in‑memory map with a JPA repository and richer DTOs.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Tool(description = "查询订单详情。支持按订单ID精确查询。返回订单的完整信息包括商品列表、价格明细、配送地址和当前状态。")
public OrderDetailDTO getOrderById(@ToolParam(description = "订单ID") String orderId) {
return orderRepository.findById(orderId)
.map(OrderDetailDTO::from)
.orElseThrow(() -> new RuntimeException("订单不存在: " + orderId));
}
@Tool(description = "统计订单数据。可以按时间范围、状态、客户维度聚合。当用户询问最近多少天有多少订单、各状态订单分布等统计问题时使用。")
public OrderStatDTO getStatistics(@ToolParam(description = "统计开始日期,格式 yyyy-MM-dd") String startDate,
@ToolParam(description = "统计结束日期,格式 yyyy-MM-dd") String endDate) {
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
return orderRepository.getStatistics(start, end);
}
}Full architecture recap
Your business system runs a Spring Boot MCP Server exposing Tools such as OrderService, UserService, and ReportService. Any AI client that supports MCP – Claude Desktop, IDEA + Claude Code, Cursor, or custom clients – can invoke these Tools directly.
Summary steps
Understand MCP protocol: JSON‑RPC 2.0 with stdio and SSE modes.
Use Spring AI MCP starter to create a server.
Annotate business methods with @Tool to expose them.
Write clear description strings so the AI can select the right Tool.
Integrate with Claude Desktop and IDEA Claude Code for verification.
In production, add permission checks, parameter validation, DTO return types, and user‑friendly error handling.
Java Web Project
Focused on Java backend technologies, trending internet tech, and the latest industry developments. The platform serves over 200,000 Java developers, inviting you to learn and exchange ideas together. Check the menu for Java learning resources.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
