Add mcp-server module: order-lookup service exposed via @McpTool/@McpResource/@McpPrompt over Streamable HTTP

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01FtpJvZfg4nvLvtzgJTDWpB
This commit is contained in:
Claude
2026-09-23 11:56:37 +00:00
parent c1fc41e0de
commit 9919da58a7
21 changed files with 704 additions and 0 deletions
@@ -0,0 +1,13 @@
package com.ankurm.mcpserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class McpServerApplication {
public static void main(String[] args) {
SpringApplication.run(McpServerApplication.class, args);
}
}
@@ -0,0 +1,11 @@
package com.ankurm.mcpserver.domain;
import java.math.BigDecimal;
import java.util.List;
/**
* The existing domain type this module pretends predates MCP entirely -- the point of this
* article is exposing a service that already looks like this, not designing one around MCP.
*/
public record Order(String id, String customer, OrderStatus status, List<String> items, BigDecimal total) {
}
@@ -0,0 +1,41 @@
package com.ankurm.mcpserver.domain;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Repository;
/**
* Stand-in for a real order-lookup service (a JPA repository, a call to another microservice,
* whatever it actually is at your shop). The MCP-facing code in {@code tools} never needs to
* change if this gets replaced with the real thing -- it depends on this interface's shape, not
* its storage.
*/
@Repository
public class OrderRepository {
private final Map<String, Order> orders = new LinkedHashMap<>();
public OrderRepository() {
orders.put("ORD-1001", new Order("ORD-1001", "Priya Nair", OrderStatus.SHIPPED,
List.of("Mechanical keyboard", "USB-C cable"), new BigDecimal("4899.00")));
orders.put("ORD-1002", new Order("ORD-1002", "Rohan Mehta", OrderStatus.PENDING,
List.of("27\" monitor"), new BigDecimal("18999.00")));
orders.put("ORD-1003", new Order("ORD-1003", "Ankita Rao", OrderStatus.DELIVERED,
List.of("Laptop stand", "Wireless mouse", "USB hub"), new BigDecimal("3450.00")));
orders.put("ORD-1004", new Order("ORD-1004", "Vikram Shah", OrderStatus.CANCELLED,
List.of("Graphics card"), new BigDecimal("42999.00")));
}
public Optional<Order> findById(String id) {
return Optional.ofNullable(orders.get(id));
}
public List<Order> findAll() {
return List.copyOf(orders.values());
}
}
@@ -0,0 +1,5 @@
package com.ankurm.mcpserver.domain;
public enum OrderStatus {
PENDING, SHIPPED, DELIVERED, CANCELLED
}
@@ -0,0 +1,53 @@
package com.ankurm.mcpserver.tools;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.ai.mcp.annotation.McpArg;
import org.springframework.ai.mcp.annotation.McpPrompt;
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.Service;
import com.ankurm.mcpserver.domain.Order;
import com.ankurm.mcpserver.domain.OrderRepository;
/**
* The existing service this article exposes over MCP. Nothing here imports an MCP class except
* the three annotations -- {@link OrderRepository} and {@link Order} are ordinary Spring beans
* and domain types that would exist whether or not an AI agent ever called them.
*/
@Service
public class OrderTools {
private final OrderRepository repository;
public OrderTools(OrderRepository repository) {
this.repository = repository;
}
@McpTool(name = "lookup_order", description = "Look up a single order by its ID and return its customer, status, items, and total.")
public Order lookupOrder(
@McpToolParam(description = "The order ID, e.g. ORD-1001", required = true) String orderId) {
return repository.findById(orderId)
.orElseThrow(() -> new IllegalArgumentException("No order with ID " + orderId));
}
@McpResource(uri = "orders://catalog", name = "order-catalog",
description = "Every order ID currently in the system, one per line -- read this first if you don't already have an order ID to look up.",
mimeType = "text/plain")
public String orderCatalog() {
return repository.findAll().stream().map(Order::id).collect(Collectors.joining("\n"));
}
@McpPrompt(name = "summarize_order", description = "A ready-made prompt asking a model to write a one-paragraph customer-support summary of one order.")
public String summarizeOrderPrompt(
@McpArg(name = "orderId", description = "The order ID to summarize, e.g. ORD-1001", required = true) String orderId) {
return "Call the lookup_order tool for order " + orderId
+ ", then write a single short paragraph a customer support agent could paste into a reply: "
+ "state the order status in plain language, list the items, and mention the total. "
+ "Do not invent any detail the tool did not return.";
}
}
@@ -0,0 +1,15 @@
spring:
application:
name: mcp-server
ai:
mcp:
server:
name: order-lookup-server
version: 1.0.0
# The jar's own spring-configuration-metadata.json lists "streamable" as this
# property's default -- but that is only the @ConfigurationProperties field default,
# and the autoconfiguration class that actually wires the /mcp endpoint is gated by
# @ConditionalOnProperty, which only ever looks at the Environment, never at a field
# default. Leave this property unset and McpServerStreamableHttpWebMvcAutoConfiguration
# never matches, full stop -- see the article for the real condition-evaluation log line.
protocol: streamable