Spring Boot Starter for building Model Context Protocol (MCP) servers with auto-configuration, annotation-based tool/resource/prompt definitions, and support for STDIO, SSE, and Streamable-HTTP transports
Production-ready examples of Spring AI MCP Server implementations.
A server that provides database query capabilities with proper error handling and progress tracking.
@Component
public class DatabaseTools {
@Autowired
private JdbcTemplate jdbcTemplate;
@McpTool(
name = "query_users",
description = "Query users from database with filters")
public CallToolResult queryUsers(
McpSyncRequestContext context,
@McpToolParam(description = "Status filter (active/inactive)", required = false) String status,
@McpToolParam(description = "Maximum results", required = false) Integer limit) {
try {
context.info("Querying users with status: " + (status != null ? status : "all"));
context.progress(0);
String sql = "SELECT * FROM users";
List<Object> params = new ArrayList<>();
if (status != null) {
sql += " WHERE status = ?";
params.add(status);
}
if (limit != null && limit > 0) {
sql += " LIMIT ?";
params.add(limit);
}
context.progress(50);
List<Map<String, Object>> users = jdbcTemplate.queryForList(sql, params.toArray());
context.progress(100);
context.info("Found " + users.size() + " users");
String result = new ObjectMapper().writeValueAsString(users);
return CallToolResult.builder()
.addTextContent(result)
.build();
} catch (Exception e) {
context.error("Query failed: " + e.getMessage());
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
}
}
@McpResource(
uri = "db://user/{id}",
name = "User Details",
description = "Get detailed user information",
mimeType = "application/json")
public ReadResourceResult getUserDetails(
McpSyncRequestContext context,
String id) {
try {
Map<String, Object> user = jdbcTemplate.queryForMap(
"SELECT * FROM users WHERE id = ?", id);
String json = new ObjectMapper().writeValueAsString(user);
return new ReadResourceResult(
List.of(new TextResourceContents(
"db://user/" + id,
"application/json",
json
))
);
} catch (EmptyResultDataAccessException e) {
context.warn("User not found: " + id);
return new ReadResourceResult(
List.of(new TextResourceContents(
"db://user/" + id,
"application/json",
"{\"error\": \"User not found\"}"
))
);
}
}
}Server for file operations with proper validation and security.
@Component
public class FileSystemTools {
@Value("${app.data-directory}")
private String dataDirectory;
@McpTool(
name = "list_files",
description = "List files in a directory")
public CallToolResult listFiles(
McpSyncRequestContext context,
@McpToolParam(description = "Directory path", required = true) String path) {
try {
// Security: Validate path is within allowed directory
Path fullPath = Paths.get(dataDirectory, path).normalize();
if (!fullPath.startsWith(dataDirectory)) {
return CallToolResult.builder()
.addTextContent("Error: Access denied - path outside allowed directory")
.isError(true)
.build();
}
context.info("Listing files in: " + path);
List<String> files = Files.list(fullPath)
.map(p -> p.getFileName().toString())
.collect(Collectors.toList());
context.info("Found " + files.size() + " files");
return CallToolResult.builder()
.addTextContent(String.join("\n", files))
.build();
} catch (IOException e) {
context.error("Failed to list files: " + e.getMessage());
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
}
}
@McpResource(
uri = "file://{path}",
name = "File Content",
description = "Read file content")
public ReadResourceResult readFile(
McpSyncRequestContext context,
String path) {
try {
Path fullPath = Paths.get(dataDirectory, path).normalize();
// Security check
if (!fullPath.startsWith(dataDirectory)) {
context.error("Access denied: " + path);
return new ReadResourceResult(
List.of(new TextResourceContents(
"file://" + path,
"text/plain",
"Error: Access denied"
))
);
}
String content = Files.readString(fullPath);
String mimeType = Files.probeContentType(fullPath);
return new ReadResourceResult(
List.of(new TextResourceContents(
"file://" + path,
mimeType != null ? mimeType : "text/plain",
content
))
);
} catch (IOException e) {
context.error("Cannot read file: " + e.getMessage());
return new ReadResourceResult(
List.of(new TextResourceContents(
"file://" + path,
"text/plain",
"Error: " + e.getMessage()
))
);
}
}
}Server that integrates with external APIs with retry logic and caching.
@Component
public class WeatherTools {
@Autowired
private RestTemplate restTemplate;
@Autowired
private CacheManager cacheManager;
@Value("${weather.api.key}")
private String apiKey;
@McpTool(
name = "get_weather",
description = "Get current weather for a location")
public CallToolResult getWeather(
McpSyncRequestContext context,
@McpToolParam(description = "City name", required = true) String city,
@McpToolParam(description = "Country code", required = false) String country) {
try {
String location = country != null ? city + "," + country : city;
context.info("Fetching weather for: " + location);
// Check cache first
Cache cache = cacheManager.getCache("weather");
String cachedData = cache.get(location, String.class);
if (cachedData != null) {
context.info("Returning cached data");
return CallToolResult.builder()
.addTextContent(cachedData)
.build();
}
context.progress(50);
// Fetch from API with retry
String url = String.format(
"https://api.weatherapi.com/v1/current.json?key=%s&q=%s",
apiKey, location);
String response = restTemplate.getForObject(url, String.class);
// Cache the result
cache.put(location, response);
context.progress(100);
context.info("Weather data fetched successfully");
return CallToolResult.builder()
.addTextContent(response)
.build();
} catch (RestClientException e) {
context.error("API request failed: " + e.getMessage());
return CallToolResult.builder()
.addTextContent("Error: Cannot fetch weather data - " + e.getMessage())
.isError(true)
.build();
}
}
}
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10))
.build();
}
}Server that orchestrates complex multi-step workflows with elicitation.
@Component
public class WorkflowTools {
@Autowired
private UserService userService;
@Autowired
private EmailService emailService;
public record UserRegistration(
String username,
String email,
String fullName,
boolean acceptTerms
) {}
@McpTool(
name = "register_user",
description = "Register a new user with interactive confirmation")
public CallToolResult registerUser(McpSyncRequestContext context) {
try {
context.info("Starting user registration workflow");
context.progress(0);
// Step 1: Elicit user information
if (!context.elicitEnabled()) {
return CallToolResult.builder()
.addTextContent("Error: Interactive registration not available")
.isError(true)
.build();
}
context.info("Requesting user information");
StructuredElicitResult<UserRegistration> result =
context.elicit(UserRegistration.class);
if (result.action() != ElicitResult.Action.ACCEPT) {
context.warn("User cancelled registration");
return CallToolResult.builder()
.addTextContent("Registration cancelled by user")
.build();
}
UserRegistration registration = result.data();
context.progress(33);
// Step 2: Validate data
context.info("Validating user data");
if (!registration.acceptTerms()) {
return CallToolResult.builder()
.addTextContent("Error: Terms must be accepted")
.isError(true)
.build();
}
if (userService.existsByUsername(registration.username())) {
return CallToolResult.builder()
.addTextContent("Error: Username already exists")
.isError(true)
.build();
}
context.progress(66);
// Step 3: Create user
context.info("Creating user account");
User user = userService.createUser(
registration.username(),
registration.email(),
registration.fullName()
);
// Step 4: Send confirmation email
context.info("Sending confirmation email");
emailService.sendWelcomeEmail(user.getEmail(), user.getUsername());
context.progress(100);
context.info("User registered successfully");
return CallToolResult.builder()
.addTextContent("User registered successfully: " + user.getId())
.build();
} catch (Exception e) {
context.error("Registration failed: " + e.getMessage());
return CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build();
}
}
}High-performance server using reactive programming.
@Component
public class ReactiveTools {
@Autowired
private WebClient webClient;
@Autowired
private R2dbcEntityTemplate template;
@McpTool(
name = "fetch_multiple_apis",
description = "Fetch data from multiple APIs in parallel")
public Mono<CallToolResult> fetchMultipleApis(
McpAsyncRequestContext context,
@McpToolParam(description = "API URLs (comma-separated)", required = true) String urls) {
return context.info("Starting parallel API fetches")
.then(Mono.defer(() -> {
List<String> urlList = Arrays.asList(urls.split(","));
// Fetch all APIs in parallel
List<Mono<String>> requests = urlList.stream()
.map(url -> webClient.get()
.uri(url.trim())
.retrieve()
.bodyToMono(String.class)
.onErrorResume(e -> Mono.just("Error: " + e.getMessage())))
.collect(Collectors.toList());
return Flux.merge(requests)
.collectList()
.flatMap(results -> {
String combined = String.join("\n---\n", results);
return context.info("All APIs fetched")
.then(Mono.just(CallToolResult.builder()
.addTextContent(combined)
.build()));
});
}))
.onErrorResume(e ->
context.error("Fetch failed: " + e.getMessage())
.then(Mono.just(CallToolResult.builder()
.addTextContent("Error: " + e.getMessage())
.isError(true)
.build()))
);
}
@McpTool(
name = "stream_database_results",
description = "Stream large result sets efficiently")
public Flux<String> streamDatabaseResults(
McpAsyncRequestContext context,
@McpToolParam(description = "Query", required = true) String query) {
return context.info("Starting database stream")
.thenMany(template.getDatabaseClient()
.sql(query)
.fetch()
.all()
.map(row -> new ObjectMapper().writeValueAsString(row))
.onErrorResume(e -> {
context.error("Stream error: " + e.getMessage()).subscribe();
return Flux.just("Error: " + e.getMessage());
}));
}
}# Server configuration
spring.ai.mcp.server.name=production-server
spring.ai.mcp.server.version=1.0.0
spring.ai.mcp.server.protocol=SSE
spring.ai.mcp.server.request-timeout=60s
# Connection pooling
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
# Caching
spring.cache.type=caffeine
spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=600s
# Logging
logging.level.org.springframework.ai.mcp=INFO
logging.level.com.myapp=DEBUG
# Security
app.data-directory=/var/data/mcp-server
weather.api.key=${WEATHER_API_KEY}